General:
Forums subtopic: App & System Services > Networking
TN3151 Choosing the right networking API
Networking Overview document — Despite the fact that this is in the archive, this is still really useful.
TLS for App Developers forums post
Choosing a Network Debugging Tool documentation
WWDC 2019 Session 712 Advances in Networking, Part 1 — This explains the concept of constrained networking, which is Apple’s preferred solution to questions like How do I check whether I’m on Wi-Fi?
TN3135 Low-level networking on watchOS
TN3179 Understanding local network privacy
Adapt to changing network conditions tech talk
Understanding Also-Ran Connections forums post
Extra-ordinary Networking forums post
Foundation networking:
Forums tags: Foundation, CFNetwork
URL Loading System documentation — NSURLSession, or URLSession in Swift, is the recommended API for HTTP[S] on Apple platforms.
Moving to Fewer, Larger Transfers forums post
Testing Background Session Code forums post
Network framework:
Forums tag: Network
Network framework documentation — Network framework is the recommended API for TCP, UDP, and QUIC on Apple platforms.
Building a custom peer-to-peer protocol sample code (aka TicTacToe)
Implementing netcat with Network Framework sample code (aka nwcat)
Configuring a Wi-Fi accessory to join a network sample code
Moving from Multipeer Connectivity to Network Framework forums post
NWEndpoint History and Advice forums post
Network Extension (including Wi-Fi on iOS):
See Network Extension Resources
Wi-Fi Fundamentals
TN3111 iOS Wi-Fi API overview
Wi-Fi Aware framework documentation
Wi-Fi on macOS:
Forums tag: Core WLAN
Core WLAN framework documentation
Wi-Fi Fundamentals
Secure networking:
Forums tags: Security
Apple Platform Security support document
Preventing Insecure Network Connections documentation — This is all about App Transport Security (ATS).
Available trusted root certificates for Apple operating systems support article
Requirements for trusted certificates in iOS 13 and macOS 10.15 support article
About upcoming limits on trusted certificates support article
Apple’s Certificate Transparency policy support article
What’s new for enterprise in iOS 18 support article — This discusses new key usage requirements.
Technote 2232 HTTPS Server Trust Evaluation
Technote 2326 Creating Certificates for TLS Testing
QA1948 HTTPS and Test Servers
Miscellaneous:
More network-related forums tags: 5G, QUIC, Bonjour
On FTP forums post
Using the Multicast Networking Additional Capability forums post
Investigating Network Latency Problems forums post
WirelessInsights framework documentation
iOS Network Signal Strength
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
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
Hello, all,
I'm new to iOS development and working on a project with the following setup:
Architecture:
Windows PC running Ubuntu (WSL) hosting a WebSocket Server with self-signed SSL
Python GUI application as a client to control iOS app
iOS app as another client on physical iPhone
Server running on wss://xxx.xxx.xxx.1:8001 (this is the mobile hotspot IP from Windows PC which the iPhone is needed to connect to as well)
Current Status:
✓ Server successfully created and running
✓ Python GUI connects and functions properly
✓ iOS app initially connects and communicates for 30 seconds
✗ iOS connection times out after 30 seconds
✗ Map updates from GUI don't sync to iOS app
Error Message in Xcode terminal:
WebSocket: Received text message
2024-11-25 15:49:03.678384-0800 iVEERS[1465:454666] Task <CD21B8AD-86D9-4984-8C48-8665CD069CC6>.<1> finished with error [-1001] Error Domain=NSURLErrorDomain Code=-1001 "The request timed out." UserInfo={_kCFStreamErrorCodeKey=-2103, _NSURLErrorFailingURLSessionTaskErrorKey=LocalWebSocketTask <CD21B8AD-86D9-4984-8C48-8665CD069CC6>.<1>, _NSURLErrorRelatedURLSessionTaskErrorKey=(
"LocalWebSocketTask <CD21B8AD-86D9-4984-8C48-8665CD069CC6>.<1>"
), NSLocalizedDescription=The request timed out., NSErrorFailingURLStringKey=wss://xxx.xxx.xxx.1:8001/, NSErrorFailingURLKey=wss://xxx.xxx.xxx.1:8001/, _kCFStreamErrorDomainKey=4}
Technical Details:
Using iOS built-in URLSessionWebSocketTask for WebSocket connection
Self-signed SSL certificate
Transport security settings configured in Info.plist
Map updates use base64 encoded PNG data
Questions:
What's causing the timeout after 30 seconds?
How can I maintain a persistent WebSocket connection?
Why aren't map updates propagating to the iOS client?
Any guidance/suggestions would be greatly appreciated. Please let me know if additional code snippets would help on what I currently have.
Hi, I am new to swift and IOS development, I was developing an app which can be used to communicating between Apple Watch and iPhone.
Something strange occurred when I was trying to observe the status of the message(UserInfo) sent by func transferUserInfo(_ userInfo: [String : Any] = [:]) -> WCSessionUserInfoTransfer.
I was trying to observe isTransferring(a boolean value) in WCSessionUserInfoTransfer which was returned by the function mentioned above, but it seems cannot be updated even if the message queue was empty, it seems to always be True.
Here is my sample code:
let transfer = session.transferUserInfo(message)
if transfer.isTransferring {
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
print("Queued message count: \(self.session.outstandingUserInfoTransfers.count), isTransferring:\(transfer.isTransferring)")
if !transfer.isTransferring {
timer.invalidate()
// irrelevant codes...
}
}
} else {
// other irrelevant codes...
}
Appreciate if anyone can help me out of this problem.
Best wishes.
Hi, I have a problem about "NSPOSIXErrorDomain Code=65 & iOS18 & Xcode 16".
I used 'CocoaAsyncSocket', '~> 7.6.5'. It works fine on iOS 15.2, But it's worried on iOS 18.3.
Before this, broadcasts can be obtained normally。 I had get socket Multicast Networking.
Please help me .
Topic:
App & System Services
SubTopic:
Networking
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.
Starting in iOS 26, two notable changes have been made to CallKit, LiveCommunicationKit, and the PushToTalk framework:
As a diagnostic aid, we're introducing new dialogs to warn apps of voip push related issue, for example when they fail to report a call or when when voip push delivery stops. The specific details of that behavior are still being determined and are likely to change over time, however, the critical point here is that these alerts are only intended to help developers debug and improve their app. Because of that, they're specifically tied to development and TestFlight signed builds, so the alert dialogs will not appear for customers running app store builds. The existing termination/crashes will still occur, but the new warning alerts will not appear.
As PushToTalk developers have previously been warned, the last unrestricted PushKit entitlement ("com.apple.developer.pushkit.unrestricted-voip.ptt") has been disabled in the iOS 26 SDK. ALL apps that link against the iOS 26 SDK which receive a voip push through PushKit and which fail to report a call to CallKit will be now be terminated by the system, as the API contract has long specified.
__
Kevin Elliott
DTS Engineer, CoreOS/Hardware
Hi everyone,
I am building a React Native iOS app that discovers audio devices on the local Wi-Fi network using UDP broadcast + mDNS/Bonjour lookup (similar to the “4Stream” app).
The app works 100% perfectly in Debug mode when installed directly from Xcode.
But once I upload it to TestFlight, the local-network features stop working completely:
UDP packets never arrive
Device discovery does not work
Bonjour/mDNS lookup returns nothing
Same phone, same Wi-Fi, same code → only Debug works, TestFlight fails
react-native-udp for UDP broadcast
react-native-dns-lookup for resolving hostnames
react-native-xml2js for parsing device responses
General:
Forums subtopic: App & System Services > Networking
DevForums tag: Network Extension
Network Extension framework documentation
Routing your VPN network traffic article
Filtering traffic by URL sample code
Filtering Network Traffic sample code
TN3120 Expected use cases for Network Extension packet tunnel providers technote
TN3134 Network Extension provider deployment technote
TN3165 Packet Filter is not API technote
Network Extension and VPN Glossary forums post
Debugging a Network Extension Provider forums post
Exporting a Developer ID Network Extension forums post
Network Extension vs ad hoc techniques on macOS forums post
Network Extension Provider Packaging forums post
NWEndpoint History and Advice forums post
Extra-ordinary Networking forums post
Wi-Fi management:
Wi-Fi Fundamentals forums post
TN3111 iOS Wi-Fi API overview technote
How to modernize your captive network developer news post
iOS Network Signal Strength forums post
See also Networking Resources.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
We are using PacketTunnel as system extension to establish vpn tunnel. The flow is like:
Create a PacketTunnelProvide to establish vpn
When tunnel gets connected add excludedRoutes by calling setTunnelNetworkSettings().
Result: The routing table is not getting updated with new excludeRoutes entries.
As per setTunnelNetworkSettings() documentation:
"This function is called by tunnel provider implementations to set the network settings of the tunnel, including IP routes, DNS servers, and virtual interface addresses depending on the tunnel type. Subclasses should not override this method. This method can be called multiple times during the lifetime of a particular tunnel. It is not necessary to call this function with nil to clear out the existing settings before calling this function with a non-nil configuration."
So we believe setTunnelNetworkSettings() should be able to set new excludeRoutes. We could see we are passing correct entries to setTunnelNetworkSettings():
{
tunnelRemoteAddress = 10.192.229.240
DNSSettings = {
protocol = cleartext
server = (
10.192.230.211,
192.168.180.15,
)
matchDomains = (
,
)
matchDomainsNoSearch = NO
}
IPv4Settings = {
configMethod = manual
addresses = (
100.100.100.17,
)
subnetMasks = (
255.255.255.255,
)
includedRoutes = (
{
destinationAddress = 1.1.1.1
destinationSubnetMask = 255.255.255.255
gatewayAddress = 100.100.100.17
},
{
destinationAddress = 2.2.2.0
destinationSubnetMask = 255.255.255.255
gatewayAddress = 100.100.100.17
},
{
destinationAddress = 11.11.11.0
destinationSubnetMask = 255.255.255.0
gatewayAddress = 100.100.100.17
},
)
excludedRoutes = (
{
destinationAddress = 170.114.52.2
destinationSubnetMask = 255.255.255.255
},
)
overridePrimary = NO
}
MTU = 1298
}
The problem is present on macOS Sequoia 15.2.
Is it a known issue? Did anyone else faced this issue?
I would like to test running some Thread Networking code on my MacOS machine:
import ThreadNetwork
let client = THClient()
let bIsPreferredAvailable = await client.isPreferredAvailable()
but I get some errors when trying to create an instance of the THClient class:
Client: -[THClient connectToXPCService]_block_invoke - CTCS XPC Client is interrupted.
Client: -[THClient getConnectionEntitlementValidity]_block_invoke - clientProxyWithErrorHandler Error: Error Domain=NSCocoaErrorDomain Code=4097 "connection to service named com.apple.ThreadNetwork.xpc" UserInfo={NSDebugDescription=connection to service named com.apple.ThreadNetwork.xpc}
Client: -[THClient init] - XPC Client Init Failed
Invalidating XPC connection.
Client: -[THClient getConnectionEntitlementValidity]_block_invoke - clientProxyWithErrorHandler Error: Error Domain=NSCocoaErrorDomain Code=4097 "connection to service named com.apple.ThreadNetwork.xpc" UserInfo={NSDebugDescription=connection to service named com.apple.ThreadNetwork.xpc}
How can I get the code to run?
Esim activation. Assuming I already have card data, I use the universal link https://esimsetup.apple.com/esim_qrcode_provisioning?carddata= to install it.
However, it always ends up in the system Settings app.
The flow: 1. Click the link -> 2. Redirect to Settings -> 3. Show activation dialog.
Is there anyway to make the activation flow stay within the app? I couldn't find any documentation for that.
This is an example from Revolut app, where the whole flow above happens without leaving the app.
I am developing an application that processes a video file stored on a server. I use URLSessionDataTask with a delegate handler to download the file.
It is not necessary to download the entire file at once. Instead, I can load small chunks of the file as needed. This approach helps minimize memory consumption.
I am trying to design a network layer that supports this behavior. Ideally, I would like to have an interface similar to:
func readMoreData(length: Int) async throws -> Data
Problems I Encountered:
It seems that URLSessionDataTask does not allow controlling how many bytes will be downloaded. It always downloads the entire request.
If I call suspend on URLSessionDataTask, the network activity does not stop, and the file keeps downloading.
If I upgrade the dataTask to a StreamTask, the file still downloads, though reading bytes can be done through the StreamTask API.
I would prefer behavior similar to AsyncHTTPClient (a Swift Server library) or Network Framework. These frameworks allow controlling the number of bytes downloaded at a time. Unfortunately, they do not fit the specific requirements of my project.
Am I correct in understanding that controlling the download process is not possible with URLSessionDataTask?
As a possible solution, I am considering using HTTP Range Requests, though this would increase the number of additional server requests, which I would like to avoid.
Topic:
App & System Services
SubTopic:
Networking
HI,
I am currently developing an app that utilizes Wi-Fi Aware.
According to the Wi-Fi Aware framework examples and the WWDC25 session on Wi-Fi Aware, discovery is handled using DevicePairingView and DevicePicker from the DeviceDiscoveryUI module.
However, these SwiftUI views present their connection UI modally when tapped. My app's design requires the ability to control the presentation of this UI programmatically, rather than relying on a user tap.
While inspecting the DeviceDiscoveryUI module, I found DDDevicePairingViewController and DDDevicePickerViewController, which appear to be the UIViewController counterparts to the SwiftUI views.
The initializer for DDDevicePairingViewController accepts a ListenerProvider, so it seems I can pass the same ListenerProvider instance that is used with the DevicePairingView.
However, the initializer for DDDevicePickerViewController requires an NWBrowser.Descriptor, which seems incompatible with the parameters used for the SwiftUI DevicePicker.
I have two main questions:
(1) Can DDDevicePairingViewController and DDDevicePickerViewController be officially used for Wi-Fi Aware pairing?
(2) Are there any plans to provide more customization or programmatic control over the DevicePairingView and DevicePicker (for example, allowing us to trigger their modal presentation programmatically)?
Thank you.
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"
On Host Names
I commonly see questions like How do I get the device’s host name? This question doesn’t make sense without more context. Apple systems have a variety of things that you might consider to be the host name:
The user-assigned device name — This is a user-visible value, for example, Guy Smiley. People set this in Settings > General > About > Name.
The local host name — This is a DNS name used by Bonjour, for example, guy-smiley.local. By default this is algorithmically derived from the user-assigned device name. On macOS, people can override this in Settings > General > Sharing > Local hostname.
The reverse DNS name associated with the various IP addresses assigned to the device’s various network interfaces
That last one is pretty much useless. You can’t get a single host name because there isn’t a single IP address. For more on that, see Don’t Try to Get the Device’s IP Address.
The other two have well-defined answers, although those answers vary by platform. I’ll talk more about that below.
Before getting to that, however, let’s look at the big picture.
Big Picture
The use cases for the user-assigned device name are pretty clear. I rarely see folks confused about that.
Another use case for this stuff is that you’ve started a server and you want to tell the user how to connect to it. I discuss this in detail in Showing Connection Information in an iOS Server.
However, most folks who run into problems like this do so because they’re suffering from one of the following misconceptions:
The device has a DNS name.
Its DNS name is unique.
Its DNS name doesn’t change.
Its DNS name is in some way useful for networking.
Some of these may be true in some specific circumstances, but none of them are true in all circumstances.
These issues are not unique to Apple platforms — if you look at the Posix spec for gethostname, it says nothing about DNS! — but folks tend to notice these problems more on Apple platforms because Apple devices are often deployed to highly dynamic network environments.
So, before you start using the APIs discussed in this post, think carefully about your assumptions.
And if you actually do want to work with DNS, there are two cases to consider:
If you’re looking for the local host name, use the APIs discussed above.
In other cases, it’s likely that the APIs in this post will not be helpful and you’d be better off focusing on DNS APIs [1].
[1] The API I recommend for this is DNS-SD. See the DNS section in TN3151 Choosing the right networking API.
macOS
To get the user-assigned device name, call the SCDynamicStoreCopyComputerName(_:_:) function. For example:
let userAssignedDeviceName = SCDynamicStoreCopyComputerName(nil, nil) as String?
To get the local host name, call the SCDynamicStoreCopyLocalHostName(_:) function. For example:
let localHostName = SCDynamicStoreCopyLocalHostName(nil) as String?
IMPORTANT This returns just the name label. To form a local host name, append .local..
Both routines return an optional result; code defensively!
If you’re displaying these values to the user, use the System Configuration framework dynamic store notification mechanism to keep your UI up to date.
iOS and Friends
On iOS, iPadOS, tvOS, and visionOS, get the user-assigned device name from the name property on UIDevice.
IMPORTANT Access to this is now restricted. For more on that, see the documentation for the com.apple.developer.device-information.user-assigned-device-name entitlement.
There is no direct mechanism to get the local host name.
Other APIs
There are a wide variety of other APIs that purport to return the host name. These include:
gethostname
The name property on NSHost [1]
The hostName property on NSProcessInfo (ProcessInfo in Swift)
These are problematic for a number of reasons:
They have a complex implementation that makes it hard to predict what value you’ll get back.
They might end up trying to infer the host name from the network environment.
The existing behaviour is hard to change due to compatibility concerns.
Some of them are marked as to-be-deprecated.
IMPORTANT The second issue is particularly problematic, because it involves synchronous DNS requests [2]. That’s slow in general. Worse yet, if the network environment is restricted in some way, these calls can be very slow, taking about 30 seconds to time out.
Given these problems, it’s generally best to avoid calling these routines at all.
[1] It also has a names property, which is a little closer to reality but still not particularly useful.
[2] Actually, that’s not true for gethostname. Rather, that call just returns whatever was last set by sethostname. This is always fast. The System Configuration framework infrastructure calls sethostname to update the host name as the system state changes.
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.
Hello,
I have a very basic quic client implementation. When you run this code with some basic quic server, you will see that we can't get a handle to stream identifier 0, but behavior is actually different when we use URLSession/URLRequest, and I can see that some information can be sent over the wire for stream identifier 0 with that implementation.
You can find both code below I'm using to test this.
I'd like to get more info about how I can use stream identifier 0 with NWMultiplexGroup, if I can't use it with NWMultiplexGroup, I need a workaround to use stream with id 0 and use multiple streams over the same connection.
import Foundation
import Network
let dispatchQueue = DispatchQueue(label: "quicConnectionQueue")
let incomingStreamQueue = DispatchQueue(label: "quicIncStreamsQueue")
let outgoingStreamQueue = DispatchQueue(label: "quicOutStreamsQueue")
let quicOptions = NWProtocolQUIC.Options()
quicOptions.alpn = ["test"]
sec_protocol_options_set_verify_block(quicOptions.securityProtocolOptions, { (sec_prot_metadata, sec_trust, complete_callback) in
complete_callback(true)
}, dispatchQueue)
let parameters = NWParameters(quic: quicOptions);
let multiplexGroup = NWMultiplexGroup(to: NWEndpoint.hostPort(host: "127.0.0.1", port: 5000))
let connectionGroup = NWConnectionGroup(with: multiplexGroup, using: parameters)
connectionGroup.stateUpdateHandler = { newState in
switch newState {
case .ready:
print("Connected using QUIC!")
let _ = createNewStream(connGroup: connectionGroup, content: "First Stream")
let _ = createNewStream(connGroup: connectionGroup, content: "Second Stream")
break
default:
print("Default hit: newState: \(newState)")
}
}
connectionGroup.newConnectionHandler = { newConnection in
// Set state update handler on incoming stream
newConnection.stateUpdateHandler = { newState in
// Handle stream states
}
// Start the incoming stream
newConnection.start(queue: incomingStreamQueue)
}
connectionGroup.start(queue: dispatchQueue)
sleep(50)
func createNewStream(connGroup: NWConnectionGroup, content: String) -> NWConnection? {
let stream = NWConnection(from: connectionGroup)
stream?.stateUpdateHandler = { streamState in
switch streamState {
case .ready:
stream?.send(content: content.data(using: .ascii), completion: .contentProcessed({ error in
print("Send completed! Error: \(String(describing: error))")
}))
print("Sent data!")
printStreamId(stream: stream)
break
default:
print("Default hit: streamState: \(streamState)")
}
}
stream?.start(queue: outgoingStreamQueue)
return stream
}
func printStreamId(stream: NWConnection?)
{
let streamMetadata = stream?.metadata(definition: NWProtocolQUIC.definition) as? NWProtocolQUIC.Metadata
print("stream Identifier: \(String(describing: streamMetadata?.streamIdentifier))")
}
URLSession/URLRequest code:
import Foundation
var networkManager = NetworkManager()
networkManager.testHTTP3Request()
sleep(5)
class NetworkManager: NSObject, URLSessionDataDelegate {
private var session: URLSession!
private var operationQueue = OperationQueue()
func testHTTP3Request() {
if self.session == nil {
let config = URLSessionConfiguration.default
config.requestCachePolicy = .reloadIgnoringLocalCacheData
self.session = URLSession(configuration: config, delegate: self, delegateQueue: operationQueue)
}
let urlStr = "https://localhost:5000"
let url = URL(string: urlStr)!
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 60.0)
request.assumesHTTP3Capable = true
self.session.dataTask(with: request) { (data, response, error) in
if let error = error as NSError? {
print("task transport error \(error.domain) / \(error.code)")
return
}
guard let data = data, let response = response as? HTTPURLResponse else {
print("task response is invalid")
return
}
guard 200 ..< 300 ~= response.statusCode else {
print("task response status code is invalid; received \(response.statusCode), but expected 2xx")
return
}
print("task finished with status \(response.statusCode), bytes \(data.count)")
}.resume()
}
}
extension NetworkManager {
func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {
let protocols = metrics.transactionMetrics.map { $0.networkProtocolName ?? "-" }
print("protocols: \(protocols)")
}
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if challenge.protectionSpace.serverTrust == nil {
completionHandler(.useCredential, nil)
} else {
let trust: SecTrust = challenge.protectionSpace.serverTrust!
let credential = URLCredential(trust: trust)
completionHandler(.useCredential, credential)
}
}
}
If I run an app with a Message Filter Extension on a handset with iOS 18.2 then it runs fine, however if I run the exact same app with no changes on a different phone which has iOS 17.6.1 installed then the following error occurs when the extension is enabled within Settings:
dyld[631]: Symbol not found: _$sSo40ILMessageFilterCapabilitiesQueryResponseC14IdentityLookupE21promotionalSubActionsSaySo0abI6ActionVGvs
Transport Layer Security (TLS) is the most important security protocol on the Internet today. Most notably, TLS puts the S into HTTPS, adding security to the otherwise insecure HTTP protocol.
IMPORTANT TLS is the successor to the Secure Sockets Layer (SSL) protocol. SSL is no longer considered secure and it’s now rarely used in practice, although many folks still say SSL when they mean TLS.
TLS is a complex protocol. Much of that complexity is hidden from app developers but there are places where it’s important to understand specific details of the protocol in order to meet your requirements. This post explains the fundamentals of TLS, concentrating on the issues that most often confuse app developers.
Note The focus of this is TLS-PKI, where PKI stands for public key infrastructure. This is the standard TLS as deployed on the wider Internet. There’s another flavour of TLS, TLS-PSK, where PSK stands for pre-shared key. This has a variety of uses, but an Apple platforms we most commonly see it with local traffic, for example, to talk to a Wi-Fi based accessory. For more on how to use TLS, both TLS-PKI and TLS-PSK, in a local context, see TLS For Accessory Developers.
Server Certificates
For standard TLS to work the server must have a digital identity, that is, the combination of a certificate and the private key matching the public key embedded in that certificate. TLS Crypto Magic™ ensures that:
The client gets a copy of the server’s certificate.
The client knows that the server holds the private key matching the public key in that certificate.
In a typical TLS handshake the server passes the client a list of certificates, where item 0 is the server’s certificate (the leaf certificate), item N is (optionally) the certificate of the certificate authority that ultimately issued that certificate (the root certificate), and items 1 through N-1 are any intermediate certificates required to build a cryptographic chain of trust from 0 to N.
Note The cryptographic chain of trust is established by means of digital signatures. Certificate X in the chain is issued by certificate X+1. The owner of certificate X+1 uses their private key to digitally sign certificate X. The client verifies this signature using the public key embedded in certificate X+1. Eventually this chain terminates in a trusted anchor, that is, a certificate that the client trusts by default. Typically this anchor is a self-signed root certificate from a certificate authority.
Note Item N is optional for reasons I’ll explain below. Also, the list of intermediate certificates may be empty (in the case where the root certificate directly issued the leaf certificate) but that’s uncommon for servers in the real world.
Once the client gets the server’s certificate, it evaluates trust on that certificate to confirm that it’s talking to the right server. There are three levels of trust evaluation here:
Basic X.509 trust evaluation checks that there’s a cryptographic chain of trust from the leaf through the intermediates to a trusted root certificate. The client has a set of trusted root certificates built in (these are from well-known certificate authorities, or CAs), and a site admin can add more via a configuration profile.
This step also checks that none of the certificates have expired, and various other more technical criteria (like the Basic Constraints extension).
Note This explains why the server does not have to include the root certificate in the list of certificates it passes to the client; the client has to have the root certificate installed if trust evaluation is to succeed.
In addition, TLS trust evaluation (per RFC 2818) checks that the DNS name that you connected to matches the DNS name in the certificate. Specifically, the DNS name must be listed in the Subject Alternative Name extension.
Note The Subject Alternative Name extension can also contain IP addresses, although that’s a much less well-trodden path. Also, historically it was common to accept DNS names in the Common Name element of the Subject but that is no longer the case on Apple platforms.
App Transport Security (ATS) adds its own security checks.
Basic X.509 and TLS trust evaluation are done for all TLS connections. ATS is only done on TLS connections made by URLSession and things layered on top URLSession (like WKWebView). In many situations you can override trust evaluation; for details, see Technote 2232 HTTPS Server Trust Evaluation). Such overrides can either tighten or loosen security. For example:
You might tighten security by checking that the server certificate was issued by a specific CA. That way, if someone manages to convince a poorly-managed CA to issue them a certificate for your server, you can detect that and fail.
You might loosen security by adding your own CA’s root certificate as a trusted anchor.
IMPORTANT If you rely on loosened security you have to disable ATS. If you leave ATS enabled, it requires that the default server trust evaluation succeeds regardless of any customisations you do.
Mutual TLS
The previous section discusses server trust evaluation, which is required for all standard TLS connections. That process describes how the client decides whether to trust the server. Mutual TLS (mTLS) is the opposite of that, that is, it’s the process by which the server decides whether to trust the client.
Note mTLS is commonly called client certificate authentication. I avoid that term because of the ongoing industry-wide confusion between certificates and digital identities. While it’s true that, in mTLS, the server authenticates the client certificate, to set this up on the client you need a digital identity, not a certificate.
mTLS authentication is optional. The server must request a certificate from the client and the client may choose to supply one or not (although if the server requests a certificate and the client doesn’t supply one it’s likely that the server will then fail the connection).
At the TLS protocol level this works much like it does with the server certificate. For the client to provide this certificate it must apply a digital identity, known as the client identity, to the connection. TLS Crypto Magic™ assures the server that, if it gets a certificate from the client, the client holds the private key associated with that certificate.
Where things diverge is in trust evaluation. Trust evaluation of the client certificate is done on the server, and the server uses its own rules to decided whether to trust a specific client certificate. For example:
Some servers do basic X.509 trust evaluation and then check that the chain of trust leads to one specific root certificate; that is, a client is trusted if it holds a digital identity whose certificate was issued by a specific CA.
Some servers just check the certificate against a list of known trusted client certificates.
When the client sends its certificate to the server it actually sends a list of certificates, much as I’ve described above for the server’s certificates. In many cases the client only needs to send item 0, that is, its leaf certificate. That’s because:
The server already has the intermediate certificates required to build a chain of trust from that leaf to its root.
There’s no point sending the root, as I discussed above in the context of server trust evaluation.
However, there are no hard and fast rules here; the server does its client trust evaluation using its own internal logic, and it’s possible that this logic might require the client to present intermediates, or indeed present the root certificate even though it’s typically redundant. If you have problems with this, you’ll have to ask the folks running the server to explain its requirements.
Note If you need to send additional certificates to the server, pass them to the certificates parameter of the method you use to create your URLCredential (typically init(identity:certificates:persistence:)).
One thing that bears repeating is that trust evaluation of the client certificate is done on the server, not the client. The client doesn’t care whether the client certificate is trusted or not. Rather, it simply passes that certificate the server and it’s up to the server to make that decision.
When a server requests a certificate from the client, it may supply a list of acceptable certificate authorities [1]. Safari uses this to filter the list of client identities it presents to the user. If you are building an HTTPS server and find that Safari doesn’t show the expected client identity, make sure you have this configured correctly. If you’re building an iOS app and want to implement a filter like Safari’s, get this list using:
The distinguishedNames property, if you’re using URLSession
The sec_protocol_metadata_access_distinguished_names routine, if you’re using Network framework
[1] See the certificate_authorities field in Section 7.4.4 of RFC 5246, and equivalent features in other TLS versions.
Self-Signed Certificates
Self-signed certificates are an ongoing source of problems with TLS. There’s only one unequivocally correct place to use a self-signed certificate: the trusted anchor provided by a certificate authority.
One place where a self-signed certificate might make sense is in a local environment, that is, securing a connection between peers without any centralised infrastructure. However, depending on the specific circumstances there may be a better option. TLS For Accessory Developers discusses this topic in detail.
Finally, it’s common for folks to use self-signed certificates for testing. I’m not a fan of that approach. Rather, I recommend the approach described in QA1948 HTTPS and Test Servers. For advice on how to set that up using just your Mac, see TN2326 Creating Certificates for TLS Testing.
TLS Standards
RFC 6101 The Secure Sockets Layer (SSL) Protocol Version 3.0 (historic)
RFC 2246 The TLS Protocol Version 1.0
RFC 4346 The Transport Layer Security (TLS) Protocol Version 1.1
RFC 5246 The Transport Layer Security (TLS) Protocol Version 1.2
RFC 8446 The Transport Layer Security (TLS) Protocol Version 1.3
RFC 4347 Datagram Transport Layer Security
RFC 6347 Datagram Transport Layer Security Version 1.2
RFC 9147 The Datagram Transport Layer Security (DTLS) Protocol Version 1.3
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
Revision History:
2025-11-21 Clearly defined the terms TLS-PKI and TLS-PSK.
2024-03-19 Adopted the term mutual TLS in preference to client certificate authentication throughout, because the latter feeds into the ongoing certificate versus digital identity confusion. Defined the term client identity. Added the Self-Signed Certificates section. Made other minor editorial changes.
2023-02-28 Added an explanation mTLS acceptable certificate authorities.
2022-12-02 Added links to the DTLS RFCs.
2022-08-24 Added links to the TLS RFCs. Made other minor editorial changes.
2022-06-03 Added a link to TLS For Accessory Developers.
2021-02-26 Fixed the formatting. Clarified that ATS only applies to URLSession. Minor editorial changes.
2020-04-17 Updated the discussion of Subject Alternative Name to account for changes in the 2019 OS releases. Minor editorial updates.
2018-10-29 Minor editorial updates.
2016-11-11 First posted.
I see a lot of folks spend a lot of time trying to get Multipeer Connectivity to work for them. My experience is that the final result is often unsatisfactory. Instead, my medium-to-long term recommendation is to use Network framework instead. This post explains how you might move from Multipeer Connectivity to Network framework.
If you have questions or comments, put them in a new thread. Place it in the App & System Services > Networking topic area and tag it with Multipeer Connectivity and Network framework.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
Moving from Multipeer Connectivity to Network Framework
Multipeer Connectivity has a number of drawbacks:
It has an opinionated networking model, where every participant in a session is a symmetric peer. Many apps work better with the traditional client/server model.
It offers good latency but poor throughput.
It doesn’t support flow control, aka back pressure, which severely constrains its utility for general-purpose networking.
It includes a number of UI components that are effectively obsolete.
It hasn’t evolved in recent years. For example, it relies on NSStream, which has been scheduled for deprecation as far as networking is concerned.
It always enables peer-to-peer Wi-Fi, something that’s not required for many apps and can impact the performance of the network (see Enable peer-to-peer Wi-Fi, below, for more about this).
Its security model requires the use of PKI — public key infrastructure, that is, digital identities and certificates — which are tricky to deploy in a peer-to-peer environment.
It has some gnarly bugs.
IMPORTANT Many folks use Multipeer Connectivity because they think it’s the only way to use peer-to-peer Wi-Fi. That’s not the case. Network framework has opt-in peer-to-peer Wi-Fi support. See Enable peer-to-peer Wi-Fi, below.
If Multipeer Connectivity is not working well for you, consider moving to Network framework. This post explains how to do that in 13 easy steps (-:
Plan for security
Select a network architecture
Create a peer identifier
Choose a protocol to match your send mode
Discover peers
Design for privacy
Configure your connections
Manage a listener
Manage a connection
Send and receive reliable messages
Send and receive best effort messages
Start a stream
Send a resource
Finally, at the end of the post you’ll find two appendices:
Final notes contains some general hints and tips.
Symbol cross reference maps symbols in the Multipeer Connectivity framework to sections of this post. Consult it if you’re not sure where to start with a specific Multipeer Connectivity construct.
Plan for security
The first thing you need to think about is security. Multipeer Connectivity offers three security models, expressed as choices in the MCEncryptionPreference enum:
.none for no security
.optional for optional security
.required for required security
For required security each peer must have a digital identity.
Optional security is largely pointless. It’s more complex than no security but doesn’t yield any benefits. So, in this post we’ll focus on the no security and required security models.
Your security choice affects the network protocols you can use:
QUIC is always secure.
WebSocket, TCP, and UDP can be used with and without TLS security.
QUIC security only supports PKI. TLS security supports both TLS-PKI and pre-shared key (PSK). You might find that TLS-PSK is easier to deploy in a peer-to-peer environment.
To configure the security of the QUIC protocol:
func quicParameters() -> NWParameters {
let quic = NWProtocolQUIC.Options(alpn: ["MyAPLN"])
let sec = quic.securityProtocolOptions
… configure `sec` here …
return NWParameters(quic: quic)
}
To enable TLS over TCP:
func tlsOverTCPParameters() -> NWParameters {
let tcp = NWProtocolTCP.Options()
let tls = NWProtocolTLS.Options()
let sec = tls.securityProtocolOptions
… configure `sec` here …
return NWParameters(tls: tls, tcp: tcp)
}
To enable TLS over UDP, also known as DTLS:
func dtlsOverUDPParameters() -> NWParameters {
let udp = NWProtocolUDP.Options()
let dtls = NWProtocolTLS.Options()
let sec = dtls.securityProtocolOptions
… configure `sec` here …
return NWParameters(dtls: dtls, udp: udp)
}
To configure TLS with a local digital identity and custom server trust evaluation:
func configureTLSPKI(sec: sec_protocol_options_t, identity: SecIdentity) {
let secIdentity = sec_identity_create(identity)!
sec_protocol_options_set_local_identity(sec, secIdentity)
if disableServerTrustEvaluation {
sec_protocol_options_set_verify_block(sec, { metadata, secTrust, completionHandler in
let trust = sec_trust_copy_ref(secTrust).takeRetainedValue()
… evaluate `trust` here …
completionHandler(true)
}, .main)
}
}
To configure TLS with a pre-shared key:
func configureTLSPSK(sec: sec_protocol_options_t, identity: Data, key: Data) {
let identityDD = identity.withUnsafeBytes { DispatchData(bytes: $0) }
let keyDD = identity.withUnsafeBytes { DispatchData(bytes: $0) }
sec_protocol_options_add_pre_shared_key(
sec,
keyDD as dispatch_data_t,
identityDD as dispatch_data_t
)
sec_protocol_options_append_tls_ciphersuite(
sec,
tls_ciphersuite_t(rawValue: TLS_PSK_WITH_AES_128_GCM_SHA256)!
)
}
Select a network architecture
Multipeer Connectivity uses a star network architecture. All peers are equal, and every peer is effectively connected to every peer. Many apps work better with the client/server model, where one peer acts on the server and all the others are clients. Network framework supports both models.
To implement a client/server network architecture with Network framework:
Designate one peer as the server and all the others as clients.
On the server, use NWListener to listen for incoming connections.
On each client, use NWConnection to made an outgoing connection to the server.
To implement a star network architecture with Network framework:
On each peer, start a listener.
And also start a connection to each of the other peers.
This is likely to generate a lot of redundant connections, as peer A connects to peer B and vice versa. You’ll need to a way to deduplicate those connections, which is the subject of the next section.
IMPORTANT While the star network architecture is more likely to create redundant connections, the client/server network architecture can generate redundant connections as well. The advice in the next section applies to both architectures.
Create a peer identifier
Multipeer Connectivity uses MCPeerID to uniquely identify each peer. There’s nothing particularly magic about MCPeerID; it’s effectively a wrapper around a large random number.
To identify each peer in Network framework, generate your own large random number. One good choice for a peer identifier is a locally generated UUID, created using the system UUID type.
Some Multipeer Connectivity apps persist their local MCPeerID value, taking advantage of its NSSecureCoding support. You can do the same with a UUID, using either its string representation or its Codable support.
IMPORTANT Before you decide to persist a peer identifier, think about the privacy implications. See Design for privacy below.
Avoid having multiple connections between peers; that’s both wasteful and potentially confusing. Use your peer identifier to deduplicate connections.
Deduplicating connections in a client/server network architecture is easy. Have each client check in with the server with its peer identifier. If the server already has a connection for that identifier, it can either close the old connection and keep the new connection, or vice versa.
Deduplicating connections in a star network architecture is a bit trickier. One option is to have each peer send its peer identifier to the other peer and then the peer with the ‘best’ identifier wins. For example, imagine that peer A makes an outgoing connection to peer B while peer B is simultaneously making an outgoing connection to peer A. When a peer receives a peer identifier from a connection, it checks for a duplicate. If it finds one, it compares the peer identifiers and then chooses a connection to drop based on that comparison:
if local peer identifier > remote peer identifier then
drop outgoing connection
else
drop incoming connection
end if
So, peer A drops its incoming connection and peer B drops its outgoing connection. Et voilà!
Choose a protocol to match your send mode
Multipeer Connectivity offers two send modes, expressed as choices in the MCSessionSendDataMode enum:
.reliable for reliable messages
.unreliable for best effort messages
Best effort is useful when sending latency-sensitive data, that is, data where retransmission is pointless because, by the retransmission arrives, the data will no longer be relevant. This is common in audio and video applications.
In Network framework, the send mode is set by the connection’s protocol:
A specific QUIC connection is either reliable or best effort.
WebSocket and TCP are reliable.
UDP is best effort.
Start with a reliable connection. In many cases you can stop there, because you never need a best effort connection.
If you’re not sure which reliable protocol to use, choose WebSocket. It has key advantages over other protocols:
It supports both security models: none and required. Moreover, its required security model supports both TLS-PKI and TLS PSK. In contrast, QUIC only supports the required security model, and within that model it only supports TLS-PKI.
It allows you to send messages over the connection. In contrast, TCP works in terms of bytes, meaning that you have to add your own framing.
If you need a best effort connection, get started with a reliable connection and use that connection to set up a parallel best effort connection. For example, you might have an exchange like this:
Peer A uses its reliable WebSocket connection to peer B to send a request for a parallel best effort UDP connection.
Peer B receives that, opens a UDP listener, and sends the UDP listener’s port number back to peer A.
Peer A opens its parallel UDP connection to that port on peer B.
Note For step 3, get peer B’s IP address from the currentPath property of the reliable WebSocket connection.
If you’re not sure which best effort protocol to use, use UDP. While it is possible to use QUIC in datagram mode, it has the same security complexities as QUIC in reliable mode.
Discover peers
Multipeer Connectivity has a types for advertising a peer’s session (MCAdvertiserAssistant) and a type for browsering for peer (MCNearbyServiceBrowser).
In Network framework, configure the listener to advertise its service by setting the service property of NWListener:
let listener: NWListener = …
listener.service = .init(type: "_example._tcp")
listener.serviceRegistrationUpdateHandler = { change in
switch change {
case .add(let endpoint):
… update UI for the added listener endpoint …
break
case .remove(let endpoint):
… update UI for the removed listener endpoint …
break
@unknown default:
break
}
}
listener.stateUpdateHandler = … handle state changes …
listener.newConnectionHandler = … handle the new connection …
listener.start(queue: .main)
This example also shows how to use the serviceRegistrationUpdateHandler to update your UI to reflect changes in the listener.
Note This example uses a service type of _example._tcp. See About service types, below, for more details on that.
To browse for services, use NWBrowser:
let browser = NWBrowser(for: .bonjour(type: "_example._tcp", domain: nil), using: .tcp)
browser.browseResultsChangedHandler = { latestResults, _ in
… update UI to show the latest results …
}
browser.stateUpdateHandler = … handle state changes …
browser.start(queue: .main)
This yields NWEndpoint values for each peer that it discovers. To connect to a given peer, create an NWConnection with that endpoint.
About service types
The examples in this post use _example._tcp for the service type. The first part, _example, is directly analogous to the serviceType value you supply when creating MCAdvertiserAssistant and MCNearbyServiceBrowser objects. The second part is either _tcp or _udp depending on the underlying transport protocol. For TCP and WebSocket, use _tcp. For UDP and QUIC, use _udp.
Service types are described in RFC 6335. If you deploy an app that uses a new service type, register that service type with IANA.
Discovery UI
Multipeer Connectivity also has UI components for advertising (MCNearbyServiceAdvertiser) and browsing (MCBrowserViewController). There’s no direct equivalent to this in Network framework. Instead, use your preferred UI framework to create a UI that best suits your requirements.
Note If you’re targeting Apple TV, check out the DeviceDiscoveryUI framework.
Discovery TXT records
The Bonjour service discovery protocol used by Network framework supports TXT records. Using these, a listener can associate metadata with its service and a browser can get that metadata for each discovered service.
To advertise a TXT record with your listener, include it it the service property value:
let listener: NWListener = …
let peerID: UUID = …
var txtRecord = NWTXTRecord()
txtRecord["peerID"] = peerID.uuidString
listener.service = .init(type: "_example._tcp", txtRecord: txtRecord.data)
To browse for services and their associated TXT records, use the .bonjourWithTXTRecord(…) descriptor:
let browser = NWBrowser(for: .bonjourWithTXTRecord(type: "_example._tcp", domain: nil), using: .tcp)
browser.browseResultsChangedHandler = { latestResults, _ in
for result in latestResults {
guard
case .bonjour(let txtRecord) = result.metadata,
let peerID = txtRecord["peerID"]
else { continue }
// … examine `result` and `peerID` …
_ = peerID
}
}
This example includes the peer identifier in the TXT record with the goal of reducing the number of duplicate connections, but that’s just one potential use for TXT records.
Design for privacy
This section lists some privacy topics to consider as you implement your app. Obviously this isn’t an exhaustive list. For general advice on this topic, see Protecting the User’s Privacy.
There can be no privacy without security. If you didn’t opt in to security with Multipeer Connectivity because you didn’t want to deal with PKI, consider the TLS-PSK options offered by Network framework. For more on this topic, see Plan for security.
When you advertise a service, the default behaviour is to use the user-assigned device name as the service name. To override that, create a service with a custom name:
let listener: NWListener = …
let name: String = …
listener.service = .init(name: name, type: "_example._tcp")
It’s not uncommon for folks to use the peer identifier as the service name. Whether that’s a good option depends on the user experience of your product:
Some products present a list of remote peers and have the user choose from that list. In that case it’s best to stick with the user-assigned device name, because that’s what the user will recognise.
Some products automatically connect to services as they discover them. In that case it’s fine to use the peer identifier as the service name, because the user won’t see it anyway.
If you stick with the user-assigned device name, consider advertising the peer identifier in your TXT record. See Discovery TXT records.
IMPORTANT Using a peer identifier in your service name or TXT record is a heuristic to reduce the number of duplicate connections. Don’t rely on it for correctness. Rather, deduplicate connections using the process described in Create a peer identifier.
There are good reasons to persist your peer identifier, but doing so isn’t great for privacy. Persisting the identifier allows for tracking of your service over time and between networks. Consider whether you need a persistent peer identifier at all. If you do, consider whether it makes sense to rotate it over time.
A persistent peer identifier is especially worrying if you use it as your service name or put it in your TXT record.
Configure your connections
Multipeer Connectivity’s symmetric architecture means that it uses a single type, MCSession, to manage the connections to all peers.
In Network framework, that role is fulfilled by two types:
NWListener to listen for incoming connections.
NWConnection to make outgoing connections.
Both types require you to supply an NWParameters value that specifies the network protocol and options to use. In addition, when creating an NWConnection you pass in an NWEndpoint to tell it the service to connect to. For example, here’s how to configure a very simple listener for TCP:
let parameters = NWParameters.tcp
let listener = try NWListener(using: parameters)
… continue setting up the listener …
And here’s how you might configure an outgoing TCP connection:
let parameters = NWParameters.tcp
let endpoint = NWEndpoint.hostPort(host: "example.com", port: 80)
let connection = NWConnection.init(to: endpoint, using: parameters)
… continue setting up the connection …
NWParameters has properties to control exactly what protocol to use and what options to use with those protocols.
To work with QUIC connections, use code like that shown in the quicParameters() example from the Security section earlier in this post.
To work with TCP connections, use the NWParameters.tcp property as shown above.
To enable TLS on your TCP connections, use code like that shown in the tlsOverTCPParameters() example from the Security section earlier in this post.
To work with WebSocket connections, insert it into the application protocols array:
let parameters = NWParameters.tcp
let ws = NWProtocolWebSocket.Options(.version13)
parameters.defaultProtocolStack.applicationProtocols.insert(ws, at: 0)
To enable TLS on your WebSocket connections, use code like that shown in the tlsOverTCPParameters() example to create your base parameters and then add the WebSocket application protocol to that.
To work with UDP connections, use the NWParameters.udp property:
let parameters = NWParameters.udp
To enable TLS on your UDP connections, use code like that shown in the dtlsOverUDPParameters() example from the Security section earlier in this post.
Enable peer-to-peer Wi-Fi
By default, Network framework doesn’t use peer-to-peer Wi-Fi. To enable that, set the includePeerToPeer property on the parameters used to create your listener and connection objects.
parameters.includePeerToPeer = true
IMPORTANT Enabling peer-to-peer Wi-Fi can impact the performance of the network. Only opt into it if it’s a significant benefit to your app.
If you enable peer-to-peer Wi-Fi, it’s critical to stop network operations as soon as you’re done with them. For example, if you’re browsing for services with peer-to-peer Wi-Fi enabled and the user picks a service, stop the browse operation immediately. Otherwise, the ongoing browse operation might affect the performance of your connection.
Manage a listener
In Network framework, use NWListener to listen for incoming connections:
let parameters: NWParameters = .tcp
… configure parameters …
let listener = try NWListener(using: parameters)
listener.service = … service details …
listener.serviceRegistrationUpdateHandler = … handle service registration changes …
listener.stateUpdateHandler = { newState in
… handle state changes …
}
listener.newConnectionHandler = { newConnection in
… handle the new connection …
}
listener.start(queue: .main)
For details on how to set up parameters, see Configure your connections. For details on how to set up up service and serviceRegistrationUpdateHandler, see Discover peers.
Network framework calls your state update handler when the listener changes state:
let listener: NWListener = …
listener.stateUpdateHandler = { newState in
switch newState {
case .setup:
// The listener has not yet started.
…
case .waiting(let error):
// The listener tried to start and failed. It might recover in the
// future.
…
case .ready:
// The listener is running.
…
case .failed(let error):
// The listener tried to start and failed irrecoverably.
…
case .cancelled:
// The listener was cancelled by you.
…
@unknown default:
break
}
}
Network framework calls your new connection handler when a client connects to it:
var connections: [NWConnection] = []
let listener: NWListener = listener
listener.newConnectionHandler = { newConnection in
… configure the new connection …
newConnection.start(queue: .main)
connections.append(newConnection)
}
IMPORTANT Don’t forget to call start(queue:) on your connections.
In Multipeer Connectivity, the session (MCSession) keeps track of all the peers you’re communicating with. With Network framework, that responsibility falls on you. This example uses a simple connections array for that purpose. In your app you may or may not need a more complex data structure. For example:
In the client/server network architecture, the client only needs to manage the connections to a single peer, the server.
On the other hand, the server must managed the connections to all client peers.
In the star network architecture, every peer must maintain a listener and connections to each of the other peers.
Understand UDP flows
Network framework handles UDP using the same NWListener and NWConnection types as it uses for TCP. However, the underlying UDP protocol is not implemented in terms of listeners and connections. To resolve this, Network framework works in terms of UDP flows. A UDP flow is defined as a bidirectional sequence of UDP datagrams with the same 4 tuple (local IP address, local port, remote IP address, and remote port). In Network framework:
Each NWConnection object manages a single UDP flow.
If an NWListener receives a UDP datagram whose 4 tuple doesn’t match any known NWConnection, it creates a new NWConnection.
Manage a connection
In Network framework, use NWConnection to start an outgoing connection:
var connections: [NWConnection] = []
let parameters: NWParameters = …
let endpoint: NWEndpoint = …
let connection = NWConnection(to: endpoint, using: parameters)
connection.stateUpdateHandler = … handle state changes …
connection.viabilityUpdateHandler = … handle viability changes …
connection.pathUpdateHandler = … handle path changes …
connection.betterPathUpdateHandler = … handle better path notifications …
connection.start(queue: .main)
connections.append(connection)
As in the listener case, you’re responsible for keeping track of this connection.
Each connection supports four different handlers. Of these, the state and viability update handlers are the most important. For information about the path update and better path handlers, see the NWConnection documentation.
Network framework calls your state update handler when the connection changes state:
let connection: NWConnection = …
connection.stateUpdateHandler = { newState in
switch newState {
case .setup:
// The connection has not yet started.
…
case .preparing:
// The connection is starting.
…
case .waiting(let error):
// The connection tried to start and failed. It might recover in the
// future.
…
case .ready:
// The connection is running.
…
case .failed(let error):
// The connection tried to start and failed irrecoverably.
…
case .cancelled:
// The connection was cancelled by you.
…
@unknown default:
break
}
}
If you a connection is in the .waiting(_:) state and you want to force an immediate retry, call the restart() method.
Network framework calls your viability update handler when its viability changes:
let connection: NWConnection = …
connection.viabilityUpdateHandler = { isViable in
… react to viability changes …
}
A connection becomes inviable when a network resource that it depends on is unavailable. A good example of this is the network interface that the connection is running over. If you have a connection running over Wi-Fi, and the user turns off Wi-Fi or moves out of range of their Wi-Fi network, any connection running over Wi-Fi becomes inviable.
The inviable state is not necessarily permanent. To continue the above example, the user might re-enable Wi-Fi or move back into range of their Wi-Fi network. If the connection becomes viable again, Network framework calls your viability update handler with a true value.
It’s a good idea to debounce the viability handler. If the connection becomes inviable, don’t close it down immediately. Rather, wait for a short while to see if it becomes viable again.
If a connection has been inviable for a while, you get to choose as to how to respond. For example, you might close the connection down or inform the user.
To close a connection, call the cancel() method. This gracefully disconnects the underlying network connection. To close a connection immediately, call the forceCancel() method. This is not something you should do as a matter of course, but it does make sense in exceptional circumstances. For example, if you’ve determined that the remote peer has gone deaf, it makes sense to cancel it in this way.
Send and receive reliable messages
In Multipeer Connectivity, a single session supports both reliable and best effort send modes. In Network framework, a connection is either reliable or best effort, depending on the underlying network protocol.
The exact mechanism for sending a message depends on the underlying network protocol. A good protocol for reliable messages is WebSocket. To send a message on a WebSocket connection:
let connection: NWConnection = …
let message: Data = …
let metadata = NWProtocolWebSocket.Metadata(opcode: .binary)
let context = NWConnection.ContentContext(identifier: "send", metadata: [metadata])
connection.send(content: message, contentContext: context, completion: .contentProcessed({ error in
// … check `error` …
_ = error
}))
In WebSocket, the content identifier is ignored. Using an arbitrary fixed value, like the send in this example, is just fine.
Multipeer Connectivity allows you to send a message to multiple peers in a single send call. In Network framework each send call targets a specific connection. To send a message to multiple peers, make a send call on the connection associated with each peer.
If your app needs to transfer arbitrary amounts of data on a connection, it must implement flow control. See Start a stream, below.
To receive messages on a WebSocket connection:
func startWebSocketReceive(on connection: NWConnection) {
connection.receiveMessage { message, _, _, error in
if let error {
… handle the error …
return
}
if let message {
… handle the incoming message …
}
startWebSocketReceive(on: connection)
}
}
IMPORTANT WebSocket preserves message boundaries, which is one of the reasons why it’s ideal for your reliable messaging connections. If you use a streaming protocol, like TCP or QUIC streams, you must do your own framing. A good way to do that is with NWProtocolFramer.
If you need the metadata associated with the message, get it from the context parameter:
connection.receiveMessage { message, context, _, error in
…
if let message,
let metadata = context?.protocolMetadata(definition: NWProtocolWebSocket.definition) as? NWProtocolWebSocket.Metadata
{
… handle the incoming message and its metadata …
}
…
}
Send and receive best effort messages
In Multipeer Connectivity, a single session supports both reliable and best effort send modes. In Network framework, a connection is either reliable or best effort, depending on the underlying network protocol.
The exact mechanism for sending a message depends on the underlying network protocol. A good protocol for best effort messages is UDP. To send a message on a UDP connection:
let connection: NWConnection = …
let message: Data = …
connection.send(content: message, completion: .idempotent)
IMPORTANT UDP datagrams have a theoretical maximum size of just under 64 KiB. However, sending a large datagram results in IP fragmentation, which is very inefficient. For this reason, Network framework prevents you from sending UDP datagrams that will be fragmented. To find the maximum supported datagram size for a connection, gets its maximumDatagramSize property.
To receive messages on a UDP connection:
func startUDPReceive(on connection: NWConnection) {
connection.receiveMessage { message, _, _, error in
if let error {
… handle the error …
return
}
if let message {
… handle the incoming message …
}
startUDPReceive(on: connection)
}
}
This is exactly the same code as you’d use for WebSocket.
Start a stream
In Multipeer Connectivity, you can ask the session to start a stream to a specific peer. There are two ways to achieve this in Network framework:
If you’re using QUIC for your reliable connection, start a new QUIC stream over that connection. This is one place that QUIC shines. You can run an arbitrary number of QUIC connections over a single QUIC connection group, and QUIC manages flow control (see below) for each connection and for the group as a whole.
If you’re using some other protocol for your reliable connection, like WebSocket, you must start a new connection. You might use TCP for this new connection, but it’s not unreasonable to use WebSocket or QUIC.
If you need to open a new connection for your stream, you can manage that process over your reliable connection. Choose a protocol to match your send mode explains the general approach for this, although in that case it’s opening a parallel best effort UDP connection rather than a parallel stream connection.
The main reason to start a new stream is that you want to send a lot of data to the remote peer. In that case you need to worry about flow control. Flow control applies to both the send and receive side.
IMPORTANT Failing to implement flow control can result in unbounded memory growth in your app. This is particularly bad on iOS, where jetsam will terminate your app if it uses too much memory.
On the send side, implement flow control by waiting for the connection to call your completion handler before generating and sending more data. For example, on a TCP connection or QUIC stream you might have code like this:
func sendNextChunk(on connection: NWConnection) {
let chunk: Data = … read next chunk from disk …
connection.send(content: chunk, completion: .contentProcessed({ error in
if let error {
… handle error …
return
}
sendNextChunk(on: connection)
}))
}
This acts like an asynchronous loop. The first send call completes immediately because the connection just copies the data to its send buffer. In response, your app generates more data. This continues until the connection’s send buffer fills up, at which point it defers calling your completion handler. Eventually, the connection moves enough data across the network to free up space in its send buffer, and calls your completion handler. Your app generates another chunk of data
For best performance, use a chunk size of at least 64 KiB. If you’re expecting to run on a fast device with a fast network, a chunk size of 1 MiB is reasonable.
Receive-side flow control is a natural extension of the standard receive pattern. For example, on a TCP connection or QUIC stream you might have code like this:
func receiveNextChunk(on connection: NWConnection) {
let chunkSize = 64 * 1024
connection.receive(minimumIncompleteLength: chunkSize, maximumLength: chunkSize) { chunk, _, isComplete, error in
if let chunk {
… write chunk to disk …
}
if isComplete {
… close the file …
return
}
if let error {
… handle the error …
return
}
receiveNextChunk(on: connection)
}
}
IMPORTANT The above is cast in terms of writing the chunk to disk. That’s important, because it prevents unbounded memory growth. If, for example, you accumulated the chunks into an in-memory buffer, that buffer could grow without bound, which risks jetsam terminating your app.
The above assumes that you can read and write chunks of data synchronously and promptly, for example, reading and writing a file on a local disk. That’s not always the case. For example, you might be writing data to an accessory over a slow interface, like Bluetooth LE. In such cases you need to read and write each chunk asynchronously.
This results in a structure where you read from an asynchronous input and write to an asynchronous output. For an example of how you might approach this, albeit in a very different context, see Handling Flow Copying.
Send a resource
In Multipeer Connectivity, you can ask the session to send a complete resource, identified by either a file or HTTP URL, to a specific peer. Network framework has no equivalent support for this, but you can implement it on top of a stream:
To send, open a stream and then read chunks of data using URLSession and send them over that stream.
To receive, open a stream and then receive chunks of data from that stream and write those chunks to disk.
In this situation it’s critical to implement flow control, as described in the previous section.
Final notes
This section collects together some general hints and tips.
Concurrency
In Multipeer Connectivity, each MCSession has its own internal queue and calls delegate callbacks on that queue. In Network framework, you get to control the queue used by each object for its callbacks. A good pattern is to have a single serial queue for all networking, including your listener and all connections.
In a simple app it’s reasonable to use the main queue for networking. If you do this, be careful not to do CPU intensive work in your networking callbacks. For example, if you receive a message that holds JPEG data, don’t decode that data on the main queue.
Overriding protocol defaults
Many network protocols, most notably TCP and QUIC, are intended to be deployed at vast scale across the wider Internet. For that reason they use default options that aren’t optimised for local networking. Consider changing these defaults in your app.
TCP has the concept of a send timeout. If you send data on a TCP connection and TCP is unable to successfully transfer it to the remote peer within the send timeout, TCP will fail the connection.
The default send timeout is infinite. TCP just keeps trying. To change this, set the connectionDropTime property.
TCP has the concept of keepalives. If a connection is idle, TCP will send traffic on the connection for two reasons:
If the connection is running through a NAT, the keepalives prevent the NAT mapping from timing out.
If the remote peer is inaccessible, the keepalives fail, which in turn causes the connection to fail. This prevents idle but dead connections from lingering indefinitely.
TCP keepalives default to disabled. To enable and configure them, set the enableKeepalive property. To configure their behaviour, set the keepaliveIdle, keepaliveCount, and keepaliveInterval properties.
Symbol cross reference
If you’re not sure where to start with a specific Multipeer Connectivity construct, find it in the tables below and follow the link to the relevant section.
[Sorry for the poor formatting here. DevForums doesn’t support tables properly, so I’ve included the tables as preformatted text.]
| For symbol | See |
| ----------------------------------- | --------------------------- |
| `MCAdvertiserAssistant` | *Discover peers* |
| `MCAdvertiserAssistantDelegate` | *Discover peers* |
| `MCBrowserViewController` | *Discover peers* |
| `MCBrowserViewControllerDelegate` | *Discover peers* |
| `MCNearbyServiceAdvertiser` | *Discover peers* |
| `MCNearbyServiceAdvertiserDelegate` | *Discover peers* |
| `MCNearbyServiceBrowser` | *Discover peers* |
| `MCNearbyServiceBrowserDelegate` | *Discover peers* |
| `MCPeerID` | *Create a peer identifier* |
| `MCSession` | See below. |
| `MCSessionDelegate` | See below. |
Within MCSession:
| For symbol | See |
| --------------------------------------------------------- | ------------------------------------ |
| `cancelConnectPeer(_:)` | *Manage a connection* |
| `connectedPeers` | *Manage a listener* |
| `connectPeer(_:withNearbyConnectionData:)` | *Manage a connection* |
| `disconnect()` | *Manage a connection* |
| `encryptionPreference` | *Plan for security* |
| `myPeerID` | *Create a peer identifier* |
| `nearbyConnectionData(forPeer:withCompletionHandler:)` | *Discover peers* |
| `securityIdentity` | *Plan for security* |
| `send(_:toPeers:with:)` | *Send and receive reliable messages* |
| `sendResource(at:withName:toPeer:withCompletionHandler:)` | *Send a resource* |
| `startStream(withName:toPeer:)` | *Start a stream* |
Within MCSessionDelegate:
| For symbol | See |
| ---------------------------------------------------------------------- | ------------------------------------ |
| `session(_:didFinishReceivingResourceWithName:fromPeer:at:withError:)` | *Send a resource* |
| `session(_:didReceive:fromPeer:)` | *Send and receive reliable messages* |
| `session(_:didReceive:withName:fromPeer:)` | *Start a stream* |
| `session(_:didReceiveCertificate:fromPeer:certificateHandler:)` | *Plan for security* |
| `session(_:didStartReceivingResourceWithName:fromPeer:with:)` | *Send a resource* |
| `session(_:peer:didChange:)` | *Manage a connection* |
Revision History
2025-04-11 Added some advice as to whether to use the peer identifier in your service name. Expanded the discussion of how to deduplicate connections in a star network architecture.
2025-03-20 Added a link to the DeviceDiscoveryUI framework to the Discovery UI section. Made other minor editorial changes.
2025-03-11 Expanded the Enable peer-to-peer Wi-Fi section to stress the importance of stopping network operations once you’re done with them. Added a link to that section from the list of Multipeer Connectivity drawbacks.
2025-03-07 First posted.
Questions about FTP crop up from time-to-time here on DevForums. In most cases I write a general “don’t use FTP” response, but I don’t have time to go into all the details. I’ve created this post as a place to collect all of those details, so I can reference them in other threads.
IMPORTANT Apple’s official position on FTP is:
All our FTP APIs have been deprecated, and you should avoid using deprecated APIs.
Apple has been slowly removing FTP support from the user-facing parts of our system. The most recent example of this is that we removed the ftp command-line tool in macOS 10.13.
You should avoid the FTP protocol and look to adopt more modern alternatives.
The rest of this post is an informational explanation of the overall FTP picture.
This post is locked so I can keep it focused. If you have questions or comments, please do create a new thread in the App & System Services > Networking subtopic and I’ll respond there.
Don’t Use FTP
FTP is a very old and very crufty protocol. Certain things that seem obvious to us now — like being able to create a GUI client that reliably shows a directory listing in a platform-independent manner — aren’t possible to do in FTP. However, by far the biggest problem with FTP is that it provides no security [1]. Specifically, the FTP protocol:
Provides no on-the-wire privacy, so anyone can see the data you transfer
Provides no client-authenticates-server authentication, so you have no idea whether you’re talking to the right server
Provides no data integrity, allowing an attacker to munge your data in transit
Transfers user names and passwords in the clear
Using FTP for anonymous downloads may be acceptable (see the explanation below) but most other uses of FTP are completely inappropriate for the modern Internet.
IMPORTANT You should only use FTP for anonymous downloads if you have an independent way to check the integrity of the data you’ve downloaded. For example, if you’re downloading a software update, you could use code signing to check its integrity. If you don’t check the integrity of the data you’ve downloaded, an attacker could substitute a malicious download instead. This would be especially bad in, say, the software update case.
These fundamental problems with the FTP protocol mean that it’s not a priority for Apple. This is reflected in the available APIs, which is the subject of the next section.
FTP APIs
Apple provides two FTP APIs:
All Apple platforms provide FTP downloads via URLSession.
Most Apple platforms (everything except watchOS) support CFFTPStream, which allows for directory listings, downloads, uploads, and directory creation.
All of these FTP APIs are now deprecated:
URLSession was deprecated for the purposes of FTP in the 2022 SDKs (macOS 13, iOS 16, iPadOS 16, tvOS 16, watchOS 9) [2].
CFFTPStream was deprecated in the 2016 SDKs (macOS 10.11, iOS 9, iPadOS 9, tvOS 9).
CFFTPStream still works about as well as it ever did, which is not particularly well. Specifically:
There is at least one known crashing bug (r. 35745763), albeit one that occurs quite infrequently.
There are clear implementation limitations — like the fact that CFFTPCreateParsedResourceListing assumes a MacRoman text encoding (r. 7420589) — that won’t be fixed.
If you’re looking for an example of how to use these APIs, check out SimpleFTPSample.
Note This sample hasn’t been updated since 2013 and is unlikely to ever be updated given Apple’s position on FTP.
The FTP support in URLSession has significant limitations:
It only supports FTP downloads; there’s no support for uploads or any other FTP operations.
It doesn’t support resumable FTP downloads [3].
It doesn’t work in background sessions. That prevents it from running FTP downloads in the background on iOS.
It’s only supported in classic loading mode. See the usesClassicLoadingMode property and the doc comments in <Foundation/NSURLSession.h>.
If Apple’s FTP APIs are insufficient for your needs, you’ll need to write or acquire your own FTP library. Before you do that, however, consider switching to an alternative protocol. After all, if you’re going to go to the trouble of importing a large FTP library into your code base, you might as well import a library for a better protocol. The next section discusses some options in this space.
Alternative Protocols
There are numerous better alternatives to FTP:
HTTPS is by far the best alternative to FTP, offering good security, good APIs on Apple platforms, good server support, and good network compatibility. Implementing traditional FTP operations over HTTPS can be a bit tricky. One possible way forward is to enable DAV extensions on the server.
FTPS is FTP over TLS (aka SSL). While FTPS adds security to the protocol, which is very important, it still inherits many of FTP’s other problems. Personally I try to avoid this protocol.
SFTP is a file transfer protocol that’s completely unrelated to FTP. It runs over SSH, making it a great alternative in many of the ad hoc setups that traditionally use FTP.
Apple doesn’t have an API for either FTPS or SFTP, although on macOS you may be able to make some headway by invoking the sftp command-line tool.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
[1] In another thread someone asked me about FTP’s other problems, those not related to security, so let’s talk about that.
One of FTP’s implicit design goals was to provide cross-platform support that exposes the target platform. You can think of FTP as being kinda like telnet. When you telnet from Unix to VMS, it doesn’t aim to abstract away VMS commands, so that you can type Unix commands at the VMS prompt. Rather, you’re expected to run VMS commands. FTP is (a bit) like that.
This choice made sense back when the FTP protocol was invented. Folks were expecting to use FTP via a command-line client, so there was a human in the loop. If they ran a command and it produced VMS-like output, that was fine because they knew that they were FTPing into a VMS machine.
However, most users today are using GUI clients, and this design choice makes it very hard to create a general GUI client for FTP. Let’s consider the simple problem of getting the contents of a directory. When you send an FTP LIST command, the server would historically run the platform native directory list command and pipe the results back to you. To create a GUI client you have to parse that data to extract the file names. Doing that is a serious challenge. Indeed, just the first step, working out the text encoding, is a challenge. Many FTP servers use UTF-8, but some use ISO-Latin-1, some use other standard encodings, some use Windows code pages, and so on.
I say “historically” above because there have been various efforts to standardise this stuff, both in the RFCs and in individual server implementations. However, if you’re building a general client you can’t rely on these efforts. After all, the reason why folks continue to use FTP is because of it widespread support.
[2] To quote the macOS 13 Ventura Release Notes:
FTP is deprecated for URLSession and related APIs. Please adopt
modern secure networking protocols such as HTTPS. (92623659)
[3] Although you can implement resumable downloads using the lower-level CFFTPStream API, courtesy of the kCFStreamPropertyFTPFileTransferOffset property.
Revision History
2025-10-06 Explained that URLSession only supports FTP in classic loading mode. Made other minor editorial changes.
2024-04-15 Added a footnote about FTP’s other problems. Made other minor editorial changes.
2022-08-09 Noted that the FTP support in URLSession is now deprecated. Made other minor editorial changes.
2021-04-06 Fixed the formatting. Fixed some links.
2018-02-23 First posted.