Skip to main content

OpenVPN Endpoint Configuration

KapeVPNDiscovery exposes VPN locations, OpenVPN endpoint configuration and the OpenVPN credentials that go with it, without going through the packet tunnel. It holds the whole discovery surface: KapeVPNDiscoveryService for stateless queries, and KapeLocationService for the observable store that publishes locations, Smart Locations, ping results and Dedicated IP status.

Reach for it when your product needs location or OpenVPN configuration data but does not run the SDK's tunnel. For connecting, use KapeVPNManager instead.

Setup​

Add the KapeVPNDiscovery product to your target:

import KapeCore // for KapeSdkHandle
import KapeVPNDiscovery // for KapeVPNDiscoveryService

Apps that already import KapeVPN get these types for free — KapeVPN re-exports KapeVPNDiscovery, so no import change is needed.

One difference if you depend on KapeVPNDiscovery alone: KapeLocationService(handle:) is provided by KapeVPN, because it reads the user's protocol selection from shared state that only KapeVPN owns. Construct it with an explicit provider instead:

let service = KapeLocationService(handle: handle, selectedProtocolProvider: { .openvpn })

Fetching OpenVPN endpoints​

let discovery = KapeVPNDiscoveryService(handle: handle)

// Which locations serve OpenVPN?
let locations = try await discovery.fetchLocations(for: .openvpn)

// Full OpenVPN payload for one of them, in the order the SDK would try them
let endpoints = try await discovery.fetchOpenVPNEndpoints(forLocation: locations[0].id)

for endpoint in endpoints {
print("\(endpoint.host):\(endpoint.port) over \(endpoint.transport.rawValue)")
if endpoint.hasXorObfuscation {
print(" obfuscated (xorValue: \(endpoint.xorValue!))")
}
// endpoint.ovpnConfiguration is the .ovpn file contents
}

The protocol is always an explicit argument. Unlike KapeLocationService, this type never reads shared state, so it works in a process that has never written a protocol selection.

KapeOpenVPNEndpoint​

PropertyTypeDescription
hostStringEndpoint address — an IPv4 or IPv6 literal
portUInt16Endpoint port
transportKapeVPNEndpointTransport.udp or .tcp
mtuUInt16Server-supplied MTU. 0 when the server did not specify one
certDnStringDistinguished Name the server certificate must present
ovpnConfigurationStringThe server-supplied OpenVPN configuration file contents
xorValueUInt64?XOR obfuscation scramble value, or nil
hasXorObfuscationBoolfalse for both nil and 0 — the backend uses 0 to mean "none"

ovpnConfiguration is not a rendering of the other properties, and the other properties are not a parse of it. The config text is authoritative and carries directives that appear nowhere else (cipher and auth among them), so treat it as the base you adapt for your OpenVPN client. The scalars let you list, filter and pick an endpoint without parsing that text; certDn and xorValue arrive as separate fields and are not read out of it. Where mtu and the config's tun-mtu disagree, tun-mtu wins.

Always test for obfuscation with hasXorObfuscation rather than xorValue != nil: the backend sends 0 as well as nil to mean "no obfuscation". The value itself is opaque — there is no documented mapping to a specific obfuscation scheme, and the SDK's own OpenVPN tunnel does not apply it.

info

The Low Level SDK's OpenVPN payload also carries an ovpnConfigTemplate field. It is intentionally not exposed — it is redundant against ovpnConfiguration, which is the configuration the tunnel itself consumes.

KapeVPNEndpointTransport is the concrete transport of one endpoint. It is a different type from KapeVPNTransport, which is the user's preference and therefore has an .automatic case.

Endpoint behaviour​

SituationResult
The backend serves no OpenVPN endpoints for the locationReturns [] — this is not an error and does not throw
The backend serves severalReturned in the Low Level SDK's attempt order, unchanged
The request fails (network, auth, subscription)Throws KapeError
An endpoint uses an address family or transport this SDK version does not knowThat endpoint is skipped; the rest keep their order

Both calls use the handle's active subscription, so authenticate and activate a subscription first.

Fetching OpenVPN credentials​

The config above authenticates with auth-user-pass. That username and password are provisioned per subscription rather than per endpoint, so they come from their own call:

guard let credentials = try await discovery.fetchOpenVPNCredentials() else {
// The active subscription has no OpenVPN credentials provisioned
return
}
print("\(credentials.username) / \(credentials.password)")

There is no identifier to pass in, and none comes back: the call reads the handle's active subscription and matches it against the account's licenses itself.

PropertyTypeDescription
usernameStringThe auth-user-pass username
passwordStringThe auth-user-pass password

These are per-license VPN credentials — not the account's login credentials, not interchangeable with them.

SituationResult
The active subscription's license carries OpenVPN credentialsReturns them
No license matches the active subscriptionReturns nil
No subscription has been activated yetReturns nil
The request fails (network, auth, licensing)Throws KapeError

Relationship to fetchEndpoints(forLocation:)​

KapeLocationService.fetchEndpoints(forLocation:) is a protocol-agnostic diagnostic view: it returns KapeVPNEndpointInfo(host, port, protocolName) for every protocol, using the protocol stored in shared state. It is unchanged and remains the right tool for "what would the tunnel try next?".

Use fetchOpenVPNEndpoints(forLocation:) when you need the OpenVPN payload itself.

The two also fail differently, so a catch does not carry over unchanged when you move between them:

CallThrows
KapeVPNDiscoveryService.fetchLocations(for:) / fetchOpenVPNEndpoints(forLocation:)KapeError
KapeLocationService.fetchLocations() / fetchEndpoints(forLocation:)KapeVPNError.sdkError(underlyingError:), wrapping a KapeError

KapeLocationService keeps its KapeVPNError signature for source compatibility; the discovery service surfaces the mapped KapeError directly. Unwrap with KapeVPNError.sdkError's associated value if you need to handle both paths with one code path.

Trying it from the CLI​

The KapeVpnCLI example wraps all three calls:

Products/KapeVpnCLI list-openvpn-endpoints <location-id> --json
Products/KapeVpnCLI list-openvpn-endpoints <location-id> --full-config
Products/KapeVpnCLI fetch-username-password