The path from Network Extension’s in-provider networking APIs to Network framework has been long and somewhat rocky. The most common cause of confusion is NWEndpoint
, where the same name can refer to two completely different types. I’ve helped a bunch of folks with this over the years, and I’ve decided to create this post to collect together all of those titbits.
If you have questions or comments, please put them in a new thread. Put it 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"
NWEndpoint History and Advice
A tale that spans three APIs, two languages, and ten years.
The NWEndpoint
type has a long and complex history, and if you’re not aware of that history you can bump into weird problems. The goal of this post is to explain the history and then offer advice on how to get around specific problems.
IMPORTANT This post focuses on NWEndpoint
, because that’s the type that causes the most problems, but there’s a similar situation with NWPath
.
The History
In iOS 9 Apple introduced the Network Extension (NE) framework, which offers a convenient way for developers to create a custom VPN transport. Network Extension types all have the NE
prefix.
Note I’m gonna use iOS versions here, just to keep the text simple. If you’re targeting some other platform, use this handy conversion table:
iOS | macOS | tvOS | watchOS | visionOS
--- + ----- + ---- + ------- + --------
9 | 10.11 | 9 | 2 | -
12 | 10.14 | 12 | 5 | -
18 | 15 | 18 | 11 | 2
At that time we also introduced in-provider networking APIs. The idea was that an NE provider could uses these Objective-C APIs to communicate with its VPN server, and thereby avoiding a bunch of ugly BSD Sockets code.
The in-provider networking APIs were limited to NE providers. Specifically, the APIs to construct an in-provider connection were placed on types that were only usable within an NE provider. For example, a packet tunnel provider could create a NWTCPConnection
object by calling -createTCPConnectionToEndpoint:enableTLS:TLSParameters:delegate:]
and -createTCPConnectionThroughTunnelToEndpoint:enableTLS:TLSParameters:delegate:
, which are both methods on NEPacketTunnelProvider
.
These in-provider networking APIs came with a number of ancillary types, including NWEndpoint
and NWPath
.
At the time we thought that we might promote these in-provider networking APIs to general-purpose networking APIs. That’s why the APIs use the NW
prefix. For example, it’s NWTCPConnection
, not NETCPConnection
.
However, plans changed. In iOS 12 Apple shipped Network framework as our recommended general-purpose networking API. This actually includes two APIs:
- A Swift API that follows Swift conventions, for example, the connection type is called
NWConnection
- A C API that follows C conventions, for example, the connection type is called
nw_connection_t
These APIs follow similar design patterns to the in-provider networking API, and thus have similar ancillary types. Specifically, there are an NWEndpoint
and nw_endpoint_t
types, both of which perform a similar role to the NWEndpoint
type in the in-provider networking API.
This was a source of some confusion in Swift, because the name NWEndpoint
could refer to either the Network framework type or the Network Extension framework type, depending on what you’d included. Fortunately you could get around this by qualifying the type as either Network.NWEndpoint
or NetworkExtension.NWEndpoint
.
The arrival of Network framework meant that it no longer made sense to promote the in-provider networking APIs to general-purposes networking APIs. The in-provider networking APIs were on the path to deprecation.
However, deprecating these APIs was actually quite tricky. Network Extension framework uses these APIs in a number of interesting ways, and so deprecating them required adding replacements. In addition, we’d needed different replacements for Swift and Objective-C, because Network framework has separate APIs for Swift and C-based languages.
In iOS 18 we tackled that problem head on. To continue the NWTCPConnection
example above, we replaced:
-createTCPConnectionToEndpoint:enableTLS:TLSParameters:delegate:]
withnw_connection_t
-createTCPConnectionThroughTunnelToEndpoint:enableTLS:TLSParameters:delegate:
withnw_connection_t
combined with a newvirtualInterface
property onNEPacketTunnelProvider
Of course that’s the Objective-C side of things. In Swift, the replacement is NWConnection
rather than nw_connection_t
, and the type of the virtualInterface
property is NWInterface
rather than nw_interface_t
.
But that’s not the full story. For the two types that use the same name in both frameworks, NWEndpoint
and NWPath
, we decided to use this opportunity to sort out that confusion. To see how we did that, check out the <NetworkExtension/NetworkExtension.apinotes>
file in the SDK. Focusing on NWEndpoint
for the moment, you’ll find two entries:
…
- Name: NWEndpoint
SwiftPrivate: true
…
SwiftVersions:
- Version: 5.0
…
- Name: NWEndpoint
SwiftPrivate: false
…
The first entry applies when you’re building with the Swift 6 language mode. This marks the type as SwiftPrivate
, which means that Swift imports it as __NWEndpoint
. That frees up the NWEndpoint
name to refer exclusively to the Network framework type.
The second entry applies when you’re building with the Swift 5 language mode. It marks the type as not SwiftPrivate
. This is a compatible measure to ensure that code written for Swift 5 continues to build.
The Advice
This sections discusses specific cases in this transition.
NWEndpoint and NWPath
In Swift 5 language mode, NWEndpoint
and NWPath
might refer to either framework, depending on what you’ve imported. Add a qualifier if there’s any ambiguity, for example, Network.NWEndpoint
or NetworkExtension.NWEndpoint
.
In Swift 6 language mode, NWEndpoint
and NWPath
always refer to the Network framework type. Add a __
prefix to get to the Network Extension type. For example, use NWEndpoint
for the Network framework type and __NWEndpoint
for the Network Extension type.
Direct and Through-Tunnel TCP Connections in Swift
To create a connection directly, simply create an NWConnection
. This support both TCP and UDP, with or without TLS.
To create a connection through the tunnel, replace code like this:
let c = self.createTCPConnectionThroughTunnel(…)
with code like this:
let params = NWParameters.tcp
params.requiredInterface = self.virtualInterface
let c = NWConnection(to: …, using: params)
This is for TCP but the same basic process applies to UDP.
UDP and App Proxies in Swift
If you’re building an app proxy, transparent proxy, or DNS proxy in Swift and need to handle UDP flows using the new API, adopt the NEAppProxyUDPFlowHandling
protocol. So, replace code like this:
class AppProxyProvider: NEAppProxyProvider {
…
override func handleNewUDPFlow(_ flow: NEAppProxyUDPFlow, initialRemoteEndpoint remoteEndpoint: NWEndpoint) -> Bool {
…
}
}
with this:
class AppProxyProvider: NEAppProxyProvider, NEAppProxyUDPFlowHandling {
…
func handleNewUDPFlow(_ flow: NEAppProxyUDPFlow, initialRemoteFlowEndpoint remoteEndpoint: NWEndpoint) -> Bool {
…
}
}
Creating a Network Rule
To create an NWHostEndpoint
, replace code like this:
let ep = NWHostEndpoint(hostname: "1.2.3.4", port: "12345")
let r = NENetworkRule(destinationHost: ep, protocol: .TCP)
with this:
let ep = NWEndpoint.hostPort(host: "1.2.3.4", port: 12345)
let r = NENetworkRule(destinationHostEndpoint: ep, protocol: .TCP)
Note how the first label of the initialiser has changed from destinationHost
to destinationHostEndpoint
.