Skip to main content

VPN Protocol

The SDK supports three VPN protocols, plus an .automatic mode that tries all three:

ProtocolDescription
.automaticTries WireGuard, Lightway and OpenVPN in the Low Level SDK's pecking order — the default
.wireguardWireGuard
.lightwayLightway — ExpressVPN's proprietary protocol, requires a LightwayCertificate in KapeSDK.plist
.openvpnOpenVPN — broadly compatible TLS-based VPN, requires four OpenVPN* keys in KapeSDK.plist

Bundled Protocols​

A protocol is only available when its KapeVPN-<Protocol>-Bridge product is linked and listed in KapePacketTunnelProvider.vpnProtocols (see Network Extension):

  • Under .automatic, endpoints of protocols you did not list are skipped; the others keep the Low Level SDK's pecking order.
  • Selecting a protocol you did not list fails the connection with PacketTunnelError.protocolNotBundled, reported like any other tunnel error.
  • An Anonymous Dedicated IP is Lightway-only: without .lightway it fails the same way under .automatic, and with dipProtocolNotSupported under another explicit protocol.
  • Settings for a protocol you did not list (Lightway cipher, Amnezia, …) are still stored, but have no effect.

The app process does not know which protocols the extension lists: location lists under .automatic may include locations served only by a protocol you did not bundle.

Reading the Selected Protocol​

let current = vpn.selectedProtocol()
// current is KapeVPNProtocol — .automatic, .wireguard, .lightway, or .openvpn

The default protocol is .automatic if no selection has been persisted yet. The App Group is read from KapeSDK.plist, so it is not a parameter.

Reading the Connected Protocol​

selectedProtocol() returns the protocol that will be used on the next connection. To read what the tunnel is actually connected with — which can differ from the selection when .automatic resolves the pecking order, or when an endpoint advance lands on a different protocol — read the connected-endpoint snapshot from the shared App Group container once the status is .connected:

let state = KapeSharedState.read(appGroup: settings.appGroupIdentifier)

// Human-readable description, e.g. "WireGuard", "Lightway+TCP", "openvpn-udp".
let description = state.connectedProtocolDescription // String?, nil when not connected

// Full endpoint snapshot.
if let endpoint = state.connectedEndpoint {
print(endpoint.protocol) // KapeVPNProtocol
print(endpoint.protocolDescription) // String
print("\(endpoint.host):\(endpoint.port)")
}

Both are populated by the Network Extension while the tunnel is up and cleared when it disconnects. Read them on .connected and treat them as nil for any other status — a stale value should not be shown mid-reconnect. (Prior SDK versions exposed this only in DEBUG builds; it is now available in all build configurations.)

Changing the Protocol​

try vpn.selectVpnProtocol(.openvpn)

Each protocol requires its own feature on your SDK license — VpnProtocolLightway, VpnProtocolWireGuard or VpnProtocolOpenVpn — and the call throws KapeFeatureNotLicensedError without it. .automatic is always allowed, and resolves to the protocols you are licensed for. See Licensed VPN features.

The selection is written to the shared App Group container (KapeSharedState) and is read by the Network Extension on its next startup. Changes apply to the next VPN connection. If the tunnel is currently active, disconnect and reconnect to use the new protocol:

try vpn.selectVpnProtocol(.openvpn)

if vpn.connectionStatus != .disconnected {
await vpn.disconnect()
}

try await vpn.connect(mode: .auto)

Lightway Requirements​

Using .lightway requires a LightwayCertificate entry in KapeSDK.plist (in both the app and extension bundles):

<key>LightwayCertificate</key>
<string>-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----</string>

The certificate is the CA certificate used to verify the Lightway server connection. It is provided to you by Kape. If this key is missing and the Lightway protocol is selected, the tunnel will fail to start.

Network Transport​

Lightway and OpenVPN servers are reachable over either UDP or TCP. By default the SDK takes whatever the server offers; selectTransport(_:) pins it to one.

vpn.selectTransport(.tcp)

let current = vpn.selectedTransport() // .automatic, .udp, or .tcp
TransportBehaviour
.automaticThe server decides — the SDK default
.udpOnly UDP endpoints are attempted
.tcpOnly TCP endpoints are attempted

The selection is written to the shared App Group container (KapeSharedState) and read by the Network Extension on its next startup. Changes apply to the next VPN connection.

Which endpoints a selection affects:

Selected protocolEffect of a forced transport
.lightway, .openvpnEndpoints not matching the transport are dropped
.wireguardNone — WireGuard is UDP-only and always passes the filter
.automaticNone — the selection is ignored entirely, so the Low Level SDK's protocol pecking order stays intact
warning

The filter is strict — there is no fallback. If a forced transport matches no endpoint for the chosen location, no configuration is produced and the connection has nothing to attempt, rather than silently using the other transport. Pair a forced transport with a location you know serves it.

An Anonymous Dedicated IP connection also honours the transport — and, unlike the shared-pool path above, it honours it even when the protocol is .automatic, because Anonymous Dedicated IP is Lightway-only and so has no protocol pecking order to preserve. A Simple Dedicated IP follows the shared-pool rules above.

An HTTPS proxy outranks this setting: under an explicit .lightway protocol with a proxy configured, TCP is forced regardless of what selectTransport(_:) was given. See HTTPS Proxy.

Bitripple is a UDP-only codec, so forcing TCP leaves it inert.

Endpoint Obfuscation​

By default the SDK attempts every endpoint a location offers, obfuscated or not. selectObfuscation(_:) restricts the connection to one kind of obfuscation:

vpn.selectObfuscation(.fm3)

let current = vpn.selectedObfuscation() // .automatic, .none, .fm1, .fm2, .fm3, or .amnezia
ObfuscationEndpoints attempted
.automaticAll of them, obfuscated or not — the SDK default
.noneUnobfuscated endpoints only (OpenVPN endpoints count as unobfuscated)
.fm1, .fm2, .fm3Lightway endpoints using that FluffyMuffins version only
.amneziaWireGuard Amnezia endpoints only

The selection is written to the shared App Group container (KapeSharedState) and read by the Network Extension each time it generates endpoints. Changes apply to the next VPN connection.

Unlike the transport, the obfuscation applies under every protocol, .automatic included. It never reorders endpoints, so the Low Level SDK's pecking order is kept among the ones that remain. Pair it with a compatible protocol: .lightway + .amnezia, or .wireguard + .fm3, matches nothing.

warning

Like the transport filter, this is strict — there is no fallback. If no endpoint of the chosen location uses the selected obfuscation, no configuration is produced and the connection has nothing to attempt. FM3 in particular is only deployed to a few locations.

It combines with the other obfuscation controls rather than overriding them:

  • .amnezia with setAmneziaEnabled(false) matches nothing — the toggle still wins.
  • selectObfuscation(_:) does not throw and carries no license key. Without VpnProtocolObfuscation on your license, obfuscated endpoints are dropped regardless, so an obfuscated selection fails with PacketTunnelError.endpointsExcludedByLicense. See Licensed VPN features.
  • An Anonymous Dedicated IP connection is always unobfuscated: it honours .automatic and .none, and any other selection leaves it nothing to attempt. A Simple Dedicated IP's endpoints go through the same filter as a regular location's.

WireGuard Obfuscation (Amnezia)​

Some WireGuard endpoints are obfuscated with Amnezia, which disguises the handshake so it is harder to identify and block. They are used automatically where a location offers them. Disable the toggle to skip them and connect over plain WireGuard instead:

vpn.setAmneziaEnabled(false)
let enabled = vpn.isAmneziaEnabled()

The preference is written to the shared App Group container (KapeSharedState) and read by the Network Extension on its next startup. WireGuard only, and changes apply to the next VPN connection. Defaults to true.

Unlike the transport and protocol selections, the Network Extension captures this value once, when the tunnel starts. switchLocation(_:) reuses the running extension, so it does not pick up a change made mid-session — disconnect and reconnect for that.

setAmneziaEnabled(_:) neither throws nor carries a license key, so it never refuses — it can only narrow what your license already allows. An SDK without VpnProtocolObfuscation drops obfuscated endpoints regardless of this toggle; see Licensed VPN features.

warning

Like the transport filter, this is strict — there is no fallback. If a location serves only Amnezia endpoints and the toggle is off, no configuration is produced and the connection has nothing to attempt, rather than falling back to an obfuscated endpoint.

OpenVPN Requirements​

Using .openvpn requires four PKI entries in KapeSDK.plist (in both the app and extension bundles):

<key>OpenVPNCertificate</key>
<string>-----BEGIN CERTIFICATE-----
... CA chain ...
-----END CERTIFICATE-----</string>

<key>OpenVPNClientCert</key>
<string>-----BEGIN CERTIFICATE-----
... client certificate ...
-----END CERTIFICATE-----</string>

<key>OpenVPNClientKey</key>
<string>-----BEGIN RSA PRIVATE KEY-----
... matching client private key ...
-----END RSA PRIVATE KEY-----</string>

<key>OpenVPNTlsAuthKey</key>
<string>-----BEGIN OpenVPN Static key V1-----
... TLS-Auth static key ...
-----END OpenVPN Static key V1-----</string>

All four keys are read as optional strings — brands that do not ship OpenVPN can omit them. If .openvpn is selected without configured PKI, the SDK surfaces a clear setupFailed error at connect time citing the missing key, rather than silently falling back to a different protocol.

The OpenVPN connection is driven by a TunnelKit-backed driver inside KapeVPN-OpenVPN. See the module's source for the driver's status state machine and the per-endpoint configuration shape.

API Reference​

KapeVPNProtocol​

public enum KapeVPNProtocol: Codable, Sendable {
case wireguard
case lightway
case openvpn
case automatic // resolves WireGuard → Lightway → OpenVPN at connect time
}

KapeVPNManager​

/// Select the VPN protocol for the next connection. Persisted to App Group shared state.
public func selectVpnProtocol(_ vpnProtocol: KapeVPNProtocol)

/// Returns the currently persisted protocol selection. Defaults to `.automatic`.
public func selectedProtocol() -> KapeVPNProtocol

/// Select the network transport for the next connection. Persisted to App Group shared state.
public func selectTransport(_ transport: KapeVPNTransport)

/// Returns the currently persisted transport selection. Defaults to `.automatic`.
public func selectedTransport() -> KapeVPNTransport

/// Select the endpoint obfuscation for the next connection. Persisted to App Group shared state.
public func selectObfuscation(_ obfuscation: KapeVPNObfuscation)

/// Returns the currently persisted obfuscation selection. Defaults to `.automatic`.
public func selectedObfuscation() -> KapeVPNObfuscation

/// Enable or disable WireGuard Amnezia obfuscation. Persisted to App Group shared state.
public func setAmneziaEnabled(_ enabled: Bool)

/// Returns the currently persisted Amnezia preference. Defaults to `true`.
public func isAmneziaEnabled() -> Bool

KapeVPNTransport​

public enum KapeVPNTransport: Codable, Sendable {
case tcp
case udp
case automatic
}

KapeVPNObfuscation​

public enum KapeVPNObfuscation: Codable, Sendable, CaseIterable {
case automatic // every endpoint, obfuscated or not
case none // unobfuscated endpoints only
case fm1
case fm2
case fm3
case amnezia
}

KapeSharedState​

/// Snapshot of the endpoint currently serving the active tunnel.
public struct ConnectedEndpoint: Codable, Equatable {
public var host: String
public var port: UInt16
public var `protocol`: KapeVPNProtocol
public var protocolDescription: String
}

// On KapeSharedState.State:

/// The endpoint currently serving the active tunnel. `nil` when not connected.
public var connectedEndpoint: ConnectedEndpoint?

/// Protocol description of the active tunnel (e.g. "Lightway+TCP", "WireGuard").
/// `nil` when not connected.
public var connectedProtocolDescription: String? { get }