On "Accessory Interface Specification CarPlay Addendum R10", it says that it is recommended that the accessory uses a MIMO (2x2) hardware configuration, does this imply that WiFi 5 and SISO (1X1) will be phased out in the near future?
When will WiFi 6 MIMO (2x2) become mandatory?
On "Accessory Interface Specification CarPlay Addendum R10", it says that Spatial Audio is mandatory. However, for aftermarket in-vehicle infotainment (IVI) system due to the number of speakers are less than 6, is it allowed not to support spatial audio for this type of aftermarket IVI system?
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
Hi,
We're hoping someone can help us determine why we're running into some odd behavior where a simple HTTP request is intermittently failing with error code NSURLErrorTimedOut (-1001)
Background:
HTTP request details:
The request is sent from a PacketTunnelProvider and is meant to be a Captive Portal check.
The request is insecure (HTTP, instead of HTTPS) but we have configured App Transport Security (ATS) to allow insecure HTTP loads from this hostname.
See info.plist excerpt below.
The request is sent using NSMutableURLRequest/NSURLSessionDataTask using an Ephemeral session configuration.
We only modify 2 properties on NSMutableURLRequest
The timeoutInterval property is set to 5 seconds.
The allowsCellularAccess property is set to NO.
No headers or other configuration are modified.
NSURLSessionDataTask completionHandler receives an NSError:
We checked the NSError's userInfo dictionary for an underlying error (NSUnderlyingErrorKey).
The underlying error shows the same code NSURLErrorTimedOut (-1001).
We haven't seen any underlying errors with code NSURLErrorAppTransportSecurityRequiresSecureConnection (-1022) .
On a laptop, we confirmed that the Captive portal check site is accessible and loads correctly.
Laptop and iOS device are on the same Wi-fi.
I've witnessed the error in the debugger, and been able to load the site on my laptop at the same time.
So, we don't have any reason to believe this is server related.
The PacketTunnelProvider is configured to only handle DNS queries and is not intercepting/routing the HTTP traffic.
The DNS query for the Captive portal request is handled correctly.
In fact, outside of the PacketTunnelProvider, all sites load in Mobile Safari.
So, we're not breaking internet on this device.
In other words, we have no reason to believe our DNS handling is interfering with the HTTP request since other HTTP requests are working as expected.
We setup CFNetwork Diagnostic Logging (https://developer.apple.com/documentation/network/debugging-https-problems-with-cfnetwork-diagnostic-logging)
In console.app, we are able to find some logging on the Timeout
See excerpt from Console.app's log below.
We confirmed that the nscurl tool did not flag the request (https://developer.apple.com/documentation/security/identifying-the-source-of-blocked-connections)
All ATS tests run with nscurl were successful.
See nscurl command used below.
Questions:
What are next steps to debug this intermittent timeout?
What should we look for in the CFNetwork Diagnostic Logging to help debug the issue further?
Thanks in advance for your help!
ATS configuration setup in both the UI and the PacketTunnel's info.plist file:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>subdomain.subdomain.example.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>
Excerpt from Console.app's log:
CFNetwork Example PacketTunnel 10836 Diagnostics default 11:30:33.029032-0700 CFNetwork Diagnostics [3:834] 11:30:32.946 {
Did Timeout: (null)
Loader: request GET http://subdomain.subdomain.example.com/content/cpcheck.txt HTTP/1.1
Timeout Interval: 5.000 seconds
init to origin load: 0.000592947s
total time: 5.00607s
total bytes: 0
} [3:834]
nscurl command
$ /usr/bin/nscurl --ats-diagnostics --verbose http://subdomain.subdomain.example.com/content/cpcheck.txt
Before iOS16, we can use
https://developer.apple.com/documentation/coretelephony/ctcarrier
But after iOS this is deprecated and has no replacement.
There are some discussions on it, eg.
https://developer.apple.com/forums/thread/714876
https://developer.apple.com/forums/thread/770400
Now I asked AI, then it provided this solution, to check the serviceCurrentRadioAccessTechnology, so it this ok to check the SIM card status?
var hasSIMCard = false
let info = CTTelephonyNetworkInfo()
if let rat = info.serviceCurrentRadioAccessTechnology,
rat.values.contains(where: { !$0.isEmpty }) {
hasSIMCard = true. // has RAT
}
BTW, I can see a lot of changes in the Core Telephony framework.
https://developer.apple.com/documentation/coretelephony
1.isSIMInserted
https://developer.apple.com/documentation/coretelephony/ctsubscriber/issiminserted
A Boolean property that indicates whether a SIM is present. iOS 18.0+ iPadOS 18.0+
This value property is true if the system finds a SIM matching the Info.plist carrier information (MCC / MNC / GID1 / GID2).
Is this ok to check SIM insert status, this seems must preconfig some info in the info.plist.
2.iOS26 provide CTCellularPlanStatus
https://developer.apple.com/documentation/coretelephony/ctcellularplanstatus
Can I use this to check SIM status?
In order to create a Message Filter Extension it is necessary to set up Shared Web Credentials.
I'd like to form an understanding of what role SWC plays when the OS is making request to the associated network service (when the extension has called deferQueryRequestToNetwork()) and how this differs from when an app directly uses Shared Web Credentials itself.
When an app is making direct use of SWC, it makes a request to obtain the user's credentials from the web site.
However in the case of a Message Filter Extension, there aren't any individual user credentials, so what is happening behind the scenes when the OS makes a server request on behalf of a Message Filtering Extension?
A more general question - the documentation for Shared Web Credentials says "Associated domains establish a secure association between domains and your app.".
Thank you
Topic:
App & System Services
SubTopic:
Networking
Tags:
iOS
SMS and Call Reporting
Authentication Services
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.
I'm using the NEHotspostConfigurationManager to join the WiFi network of a configured accessory.
While this is all nice and dandy, I wonder why I'm still connected to said WiFi when I (force-)close the app. Wouldn't it be more useful to reconnect to the last network before?
Topic:
App & System Services
SubTopic:
Networking
I am trying to activate an application which sends my serial number to a server. The send is being blocked. The app is signed but not sandboxed.
I am running Sequoia on a recent iMac. My network firewall is off and I do not have any third party virus software. I have selected Allow Applications from App Store & Known Developers.
My local network is wifi using the eero product. There is no firewall or virus scanning installed with this product.
Under what circumstances will Mac OS block outgoing internet connections from a non-sandboxed app? How else could the outgoing connection be blocked?
Topic:
App & System Services
SubTopic:
Networking
Hello, I have encountered an issue with an iPhone 15PM with iOS 18.5. The NSHTTPCookieStorage failed to clear cookies, but even after clearing them, I was still able to retrieve them. However, on the same system
It is normal on iPhone 14PM. I would like to know the specific reason and whether there are any adaptation related issues. Following code:
NSHTTPCookie *cookie;
NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (cookie in [storage cookies]) {
[storage deleteCookie:cookie];
}
Hi, I am making a AI-Powered app that makes api requests to the openai API. However, for security, I set up a vercel backend that handles the API calls securely, while my frontend makes a call to my vercel-hosted https endpoint. Interestingly, whenever I try to make that call on my device, an iPhone, I get this error:
Task <91AE4DE0-2845-4348-89B4-D3DD1CF51B65>.<10> finished with error [-1003] Error Domain=NSURLErrorDomain Code=-1003 "A server with the specified hostname could not be found." UserInfo={_kCFStreamErrorCodeKey=-72000, NSUnderlyingError=0x1435783f0 {Error Domain=kCFErrorDomainCFNetwork Code=-1003 "(null)" UserInfo={_kCFStreamErrorDomainKey=10, _kCFStreamErrorCodeKey=-72000, _NSURLErrorNWResolutionReportKey=Resolved 0 endpoints in 3ms using unknown from query, _NSURLErrorNWPathKey=satisfied (Path is satisfied), interface: pdp_ip0[lte], ipv4, ipv6, dns, expensive, uses cell}}, _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <91AE4DE0-2845-4348-89B4-D3DD1CF51B65>.<10>, _NSURLErrorRelatedURLSessionTaskErrorKey=(
"LocalDataTask <91AE4DE0-2845-4348-89B4-D3DD1CF51B65>.<10>"
), NSLocalizedDescription=A server with the specified hostname could not be found., NSErrorFailingURLStringKey=https://[my endpoint], NSErrorFailingURLKey=https://[my endpoint], _kCFStreamErrorDomainKey=10}
I'm completely stuck because when I directly make https requests to other api's like openai's endpoint, without the proxy, it finds the server completely fine. Running my endpoint on terminal with curl also works as intended, as I see api key usages. But for some reason, on my project, it does not work. I've looked through almost every single post I could find online, but a lot all of the solutions are outdated and unhelpful.
I'm willing to schedule a call, meeting, whatever to resolve this issue and get help more in depth as well.
I am wondering wether iOS allow apps to detect users' proxy.
Topic:
App & System Services
SubTopic:
Networking
Hello everyone,
I'm trying to figure out how to transmit a UIImage (png or tiff) securely to an application running in my desktop browser (Mac or PC). The desktop application and iOS app would potentially be running on the same local network (iOS hotspot or something) or have no internet connection at all.
I'm trying to securely send over an image that the running desktop app could ingest. I was thinking something like a local server securely accepting image data from an iPhone.
Any suggestions ideas or where to look for more info would be greatly appreciated!
Thank you for your help.
We are using the [NEHotspotHelper supportedNetworkInterfaces] to get the Wi-Fi interface in our app, but it occasionally crashes on some devices with the following stack trace:
0 CaptiveNetwork 0x0000000221d87a4c ServerConnectionGetHandlerQueue + 0 (ServerConnection.c:509)
1 CaptiveNetwork 0x0000000221d8577c CNPluginCopySupportedInterfaces + 180 (CNPlugin.c:457)
2
NetworkExtension 0x00000001b0446618 +[NEHotspotHelper supportedNetworkInterfaces] + 32 (NEHotspotHelper.m:563)
It seems like the crash is happening on apple's api of supportedNetworkInterfaces. We would like to understand the cause of the crash.
Hi there, I am working on an app that configures a PacketTunnelProvider to establish a VPN connection. Unfortunately, while a VPN connection is established, I am unable to update the app via testflight. Downloading other app updates works fine.
I noticed that after I receive the alert that updating failed, the vpn badge appears at the top of my screen (the same ux that occurs when the connection is first established). So it's almost like it tried to close the tunnel, and seeing that the app update failed it restablishes the tunnel.
I am unsure of why I would not be able to update my app. Maybe stopTunnel is not being called with NEProviderStopReason.appUpdate?
multicast sockets fail to send/receive on macosx, errno 65 "no route to host".
Wireshark and Terminal.app (which have root privileges) both show incoming multicast traffic just fine.
Normal UDP broadcast sockets have no problems.
Toggling the Security&Privacy -> Local Network setting may fix the problem for some Users.
There is no pattern for when multicast socket fails.
Sometimes, recreating the sockets fix the problem.
Restart the app, sometimes multicast fails, sometimes success (intermittent, no pattern).
Reboot machine (intermittent fail)
Create a fresh new user on machine, install single version of app, give app permission. (intermittent fail, same as above).
We have all the normal entitlements / notarized app.
Similar posts here
see FB16923535, Related to FB16512666
https://forum.xojo.com/t/udp-multicast-receive-on-mac-failing-intermittant/83221
see my post from 2012 "distinguishing between SENDING sockets and RECEIVING sockets" for source code example of how we bind multicast sockets. Our other socket code is standard "Stevens, et al." code. The bind() is the call that fails in this case. https://stackoverflow.com/questions/10692956/what-does-it-mean-to-bind-a-multicast-udp-socket . Note that this post from 2012 is still relevant, and that it is a workaround to a longstanding Apple bug that was never fixed. Namely, "Without this fix, multicast sending will intermittently get sendto() errno 'No route to host'. If anyone can shed light on why unplugging a DHCP gateway causes Mac OS X multicast SENDING sockets to get confused, I would love to hear it."
This may be a hint as to the underlying bug that Apple really needs to fix, but if it's not, then please Apple, fix the Sequoia bug first. These are probably different bugs because in one case, sendto() fails when a socket becomes "unbound" after you unplug an unrelated network cable. In this case, bind() fails, so sendto() is never even called.
Note, that we have also tried to use other implementations for network discovery, including Bonjour, CFNetwork, etc. Bonjour fails intermittently, and also suffers from both bugs mentioned above, amongst others.
I'm a long-time developer, but pretty new to Swift. I'm trying to get information from a web service (and found code online that I adjusted to build the function below). (Note: AAA_Result -- referenced towards the end -- is another class in my project)
Trouble is, I'm getting the subject error on the call to session.dataTask. Any help/suggestions/doc pointers will be greatly appreciated!!!
var result: Bool = false
var cancellable: AnyCancellable?
self.name = name
let params = "json={\"\"}}" // removed json details
let base_url = URL(string: "https://aaa.yyy.com?params=\(params)&format=json")! // removed URL specifics
do {
let task = URLSession.shared.dataTask(with: base_url) { data, response, error in
if let error = error {
print("Error: \(error)")
}
guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode)
else {
print("Error \(String(describing: response))")
}
do {
let decoder = JSONDecoder()
let ar = try decoder.decode(AAA_Result.self, from: response.value)
// removed specific details...
result = true
}
catch {
print(error)
}
}
task.resume()
}
catch {
print(error)
}
return result
}
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.
I'm developing a Matter-over-thread generic switch with 2 generic switch endpoints. This is configured as an Intermittently Connected Device with Long Idle Time.
I have an Apple TV serving as the thread border router.
I'm able to commission the device successfully in the Home app and assign actions to each of the buttons however when the device is rebooted the subscription doesn't appear to resume successfully and the buttons no longer work.
I've tested this on various SOC's with their respective SDKs including ESP32-C6, nrf52840 and EFR32MG24 and the behaviour was consistent across all of them.
It was working originally when I first started out on the ESP32-C6, then the issue popped up first when I was testing the nrf52840. In that SDK I set persistent subscriptions explicitly and it seemed to resolve the issue until it popped up again when I found that unplugging and restarting the Apple TV completely which appeared to fix the issue with subscriptions not resuming.
Recently I've added a Home Pod Mini Gen 2 to the matter fabric so there are now two TBR on the network and restarting both the Apple TV and the HomePod doesn't appear to resolve the issue anymore and the subscriptions are not resuming across all three SOC's on device reboot
I'm wondering if there might be something preventing the subscriptions from resuming?
Every now and again folks notice that Network framework seems to create an unexpected number of connections on the wire. This post explains why that happens and what you should do about it.
If you have questions or comments, put them in a new thread here on the forums. Use the App & System Services > Networking topic area and the Network tag.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
Understanding Also-Ran Connections
Network framework implements the Happy Eyeballs algorithm. That might create more on-the-wire connections than you expect. There are two common places where folks notice this:
When looking at a packet trace
When implementing a listener
Imagine that you’ve implemented a TCP server using NWListener and you connect to it from a client using NWConnection. In many situations there are multiple network paths between the client and the server. For example, on a local network there’s always at least two paths: the link-local IPv6 path and either an infrastructure IPv4 path or the link-local IPv4 path.
When you start your NWConnection, Network framework’s Happy Eyeballs algorithm might [1] start a TCP connection for each of these paths. It then races those connections. The one that connects first is the ‘winner’, and Network framework uses that connection for your traffic. Once it has a winner, the other connections, the also-ran connections, are redundant, and Network framework just closes them.
You can observe this behaviour on the client side by looking in the system log. Many Network framework log entries (subsystem com.apple.network) contain a connection identifier. For example C8 is the eighth connection started by this process. Each connection may have child connections (C8.1, C8.2, …) and grandchild connections (C8.1.1, C8.1.2, …), and so on. You’ll see state transitions for these child connections occurring in parallel. For example, the following log entries show that C8 is racing the connection of two grandchild connections, C8.1.1 and C8.1.2:
type: debug
time: 12:22:26.825331+0100
process: TestAlsoRanConnections
subsystem: com.apple.network
category: connection
message: nw_socket_connect [C8.1.1:1] Calling connectx(…)
type: debug
time: 12:22:26.964150+0100
process: TestAlsoRanConnections
subsystem: com.apple.network
category: connection
message: nw_socket_connect [C8.1.2:1] Calling connectx(…)
Note For more information about accessing the system log, see Your Friend the System Log.
You also see this on the server side, but in this case each connection is visible to your code. When you connect from the client, Network framework calls your listener’s new connection handler with multiple connections. One of those is the winning connection and you’ll receive traffic on it. The others are the also-ran connections, and they close promptly.
IMPORTANT Depending on network conditions there may be no also-ran connections. Or there may be lots of them. If you want to test the also-ran connection case, use Network Link Conditioner to add a bunch of delay to your packets.
You don’t need to write special code to handle also-ran connections. From the perspective of your listener, these are simply connections that open and then immediately close. There’s no difference between an also-ran connection and, say, a connection from a client that immediately crashes. Or a connection generated by someone doing a port scan. Your server must be resilient to such things.
However, the presence of these also-ran connections can be confusing, especially if you’re just getting started with Network framework, and hence this post.
[1] This is “might” because the exact behaviour depends on network conditions. More on that below.
Hi folks, I'm building an iOS companion app to a local hosted server app (hosted on 0.0.0.0). The MacOS app locally connects to this server hosted, and I took the approach of advertising the server using a Daemon and BonjourwithTXT(for port) and then net service to resolve a local name. Unfortunately if there's not enough time given after the iPhone/iPad is plugged in (usb or ethernet), the app will cycle through attempts and disconnects many times before connecting and I'm trying to find a way to only connect when a viable en interface is available.
I've run into a weird thing in which the en interface only becomes seen on the NWMonitor after multiple connection attempts have been made and failed. If I screen for en before connecting it simply never appears. Is there any way to handle this such that my app can intelligently wait for an en connection before trying to connect? Attaching my code although I have tried a few other setups but none has been perfect.
func startMonitoringAndBrowse() {
DebugLogger.shared.append("Starting Bonjour + Ethernet monitoring")
if !browserStarted {
let params = NWParameters.tcp
params.includePeerToPeer = false
params.requiredInterfaceType = .wiredEthernet
browser = NWBrowser(for: .bonjourWithTXTRecord(type: "_mytcpapp._tcp", domain: nil), using: params)
browser?.stateUpdateHandler = { state in
if case .ready = state {
DebugLogger.shared.append("Bonjour browser ready.")
}
}
browser?.browseResultsChangedHandler = { results, _ in
self.handleBrowseResults(results)
}
browser?.start(queue: .main)
browserStarted = true
}
// Start monitoring for wired ethernet
monitor = NWPathMonitor()
monitor?.pathUpdateHandler = { path in
let hasEthernet = path.availableInterfaces.contains { $0.type == .wiredEthernet }
let ethernetInUse = path.usesInterfaceType(.wiredEthernet)
DebugLogger.shared.append("""
NWPathMonitor:
- Status: \(path.status)
- Interfaces: \(path.availableInterfaces.map { "\($0.name)[\($0.type)]" }.joined(separator: ", "))
- Wired Ethernet: \(hasEthernet), In Use: \(ethernetInUse)
""")
self.tryToConnectIfReady()
self.stopMonitoring()
}
monitor?.start(queue: monitorQueue)
}
// MARK: - Internal Logic
private func handleBrowseResults(_ results: Set<NWBrowser.Result>) {
guard !self.isResolving, !self.hasResolvedService else { return }
for result in results {
guard case let .bonjour(txtRecord) = result.metadata,
let portString = txtRecord["actual_port"],
let actualPort = Int(portString),
case let .service(name, type, domain, _) = result.endpoint else {
continue
}
DebugLogger.shared.append("Bonjour result — port: \(actualPort)")
self.resolvedPort = actualPort
self.isResolving = true
self.resolveWithNetService(name: name, type: type, domain: domain)
break
}
}
private func resolveWithNetService(name: String, type: String, domain: String) {
let netService = NetService(domain: domain, type: type, name: name)
netService.delegate = self
netService.includesPeerToPeer = false
netService.resolve(withTimeout: 5.0)
resolvingNetService = netService
DebugLogger.shared.append("Resolving NetService: \(name).\(type)\(domain)")
}
private func tryToConnectIfReady() {
guard hasResolvedService,
let host = resolvedHost, let port = resolvedPort else { return }
DebugLogger.shared.append("Attempting to connect: \(host):\(port)")
discoveredIP = host
discoveredPort = port
connectionPublisher.send(.connecting(ip: host, port: port))
stopBrowsing()
socketManager.connectToServer(ip: host, port: port)
hasResolvedService = false
}
}
// MARK: - NetServiceDelegate
extension BonjourManager: NetServiceDelegate {
func netServiceDidResolveAddress(_ sender: NetService) {
guard let hostname = sender.hostName else {
DebugLogger.shared.append("Resolved service with no hostname")
return
}
DebugLogger.shared.append("Resolved NetService hostname: \(hostname)")
resolvedHost = hostname
isResolving = false
hasResolvedService = true
tryToConnectIfReady()
}
func netService(_ sender: NetService, didNotResolve errorDict: [String : NSNumber]) {
DebugLogger.shared.append("NetService failed to resolve: \(errorDict)")
}
}
Hi there,
We are facing some issues regarding TLS connectivity:
Starting with iOS 26, the operating system refuses to open TLS sockets to local devices with self-signed certificates over Wi-Fi. In this situation, connection is no longer possible, even if the device is detected on the network with Bonjour.
We have not found a workaround for this problem.
We've tryied those solutions without success:
Added the 'NSAppTransportSecurity' key to the info.plist file, testing all its items, such as "NSAllowsLocalNetworking", "NSExceptionDomains", etc.
Various code changes to use properties such as "sec_protocol_options_set_local_identity" and "sec_protocol_options_set_tls_server_name" to no avail.
Brutally import the certificate files into the project and load them via, for example, "Bundle.main.url(forResource: "nice_INTERFACE_server_cert", withExtension: "crt")", using methods such as sec_trust_copy_ref and SecCertificateCopyData.
Download the .pem or .crt files to the iPhone, install them (now visible under "VPN & Device Management"), and then flag them as trusted by going to "Settings -> General -> Info -> Trust". certificates"
The most critical part seems to be the line
sec_protocol_options_set_verify_block(tlsOptions.securityProtocolOptions, { $2(true) }, queue)
whose purpose is to bypass certificate checks and validate all of them (as apps already do). However, on iOS26, if I set a breakpoint on leg$2(true),` it never gets there, while on iOS 18, it does.
I'll leave as example the part of the code that was tested the most below. Currently, on iOS26, the handler systematically falls back to .cancelled:
func startConnection(host: String, port: UInt16) {
self.queue = DispatchQueue(label: "socketQueue")
let tlsOptions = NWProtocolTLS.Options()
sec_protocol_options_set_verify_block(tlsOptions.securityProtocolOptions, { $2(true) }, queue)
let parameters = NWParameters(tls: tlsOptions)
self.nwConnection = NWConnection(host: .init(host), port: .init(rawValue: port)!, using: parameters)
self.nwConnection.stateUpdateHandler = { [weak self] state in
switch state {
case .setup:
break
case .waiting(let error):
self?.connectionDidFail(error: error)
case .preparing:
break
case .ready:
self?.didConnectSubject.onNext(Void())
case .failed(let error):
self?.connectionDidFail(error: error)
case .cancelled:
self?.didDisconnectSubject.onNext(nil)
@unknown default:
break
}
}
self.setupReceive()
self.nwConnection.start(queue: queue)
}
These are the prints made during the procedure. The ones with the dot are from the app, while the ones without are warnings/info from Xcode:
🔵 INFO WifiNetworkManager.connect():52 - Try to connect onto the interface access point with ssid NiceProView4A9151_AP
🔵 INFO WifiNetworkManager.connect():68 - Connected to NiceProView4A9151_AP
tcp_output [C13:2] flags=[R.] seq=215593821, ack=430284980, win=4096 state=CLOSED rcv_nxt=430284980, snd_una=215593821
nw_endpoint_flow_failed_with_error [C13 192.168.0.1:443 in_progress channel-flow (satisfied (Path is satisfied), viable, interface: en0[802.11], dns, uses wifi, LQM: unknown)] already failing, returning
nw_connection_copy_protocol_metadata_internal_block_invoke [C13] Client called nw_connection_copy_protocol_metadata_internal on unconnected nw_connection
nw_connection_copy_protocol_metadata_internal_block_invoke [C13] Client called nw_connection_copy_protocol_metadata_internal on unconnected nw_connection
nw_connection_copy_connected_local_endpoint_block_invoke [C13] Client called nw_connection_copy_connected_local_endpoint on unconnected nw_connection
nw_connection_copy_connected_remote_endpoint_block_invoke [C13] Client called nw_connection_copy_connected_remote_endpoint on unconnected nw_connection
nw_connection_copy_protocol_metadata_internal_block_invoke [C14] Client called nw_connection_copy_protocol_metadata_internal on unconnected nw_connection
nw_connection_copy_protocol_metadata_internal_block_invoke [C14] Client called nw_connection_copy_protocol_metadata_internal on unconnected nw_connection
nw_connection_copy_connected_local_endpoint_block_invoke [C14] Client called nw_connection_copy_connected_local_endpoint on unconnected nw_connection
nw_connection_copy_connected_remote_endpoint_block_invoke [C14] Client called nw_connection_copy_connected_remote_endpoint on unconnected nw_connection
[C14 192.168.0.1:443 tcp, tls, attribution: developer] is already cancelled, ignoring cancel
[C14 192.168.0.1:443 tcp, tls, attribution: developer] is already cancelled, ignoring cancel
nw_connection_copy_protocol_metadata_internal_block_invoke [C15] Client called nw_connection_copy_protocol_metadata_internal on unconnected nw_connection
nw_connection_copy_protocol_metadata_internal_block_invoke [C15] Client called nw_connection_copy_protocol_metadata_internal on unconnected nw_connection
nw_connection_copy_connected_local_endpoint_block_invoke [C15] Client called nw_connection_copy_connected_local_endpoint on unconnected nw_connection
nw_connection_copy_connected_remote_endpoint_block_invoke [C15] Client called nw_connection_copy_connected_remote_endpoint on unconnected nw_connection
nw_connection_copy_protocol_metadata_internal_block_invoke [C16] Client called nw_connection_copy_protocol_metadata_internal on unconnected nw_connection
nw_connection_copy_protocol_metadata_internal_block_invoke [C16] Client called nw_connection_copy_protocol_metadata_internal on unconnected nw_connection
nw_connection_copy_connected_local_endpoint_block_invoke [C16] Client called nw_connection_copy_connected_local_endpoint on unconnected nw_connection
nw_connection_copy_connected_remote_endpoint_block_invoke [C16] Client called nw_connection_copy_connected_remote_endpoint on unconnected nw_connection
[C16 192.168.0.1:443 tcp, tls, attribution: developer] is already cancelled, ignoring cancel
[C16 192.168.0.1:443 tcp, tls, attribution: developer] is already cancelled, ignoring cancel
🔴 ERROR InterfaceDisconnectedViewModel.connect():51 - Sequence timeout.
Topic:
App & System Services
SubTopic:
Networking
Tags:
Foundation
Developer Tools
Nearby Interaction
iOS