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
| Property | Type | Description |
|---|---|---|
host | String | Endpoint address — an IPv4 or IPv6 literal |
port | UInt16 | Endpoint port |
transport | KapeVPNEndpointTransport | .udp or .tcp |
mtu | UInt16 | Server-supplied MTU. 0 when the server did not specify one |
certDn | String | Distinguished Name the server certificate must present |
ovpnConfiguration | String | The server-supplied OpenVPN configuration file contents |
xorValue | UInt64? | XOR obfuscation scramble value, or nil |
hasXorObfuscation | Bool | false 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.
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
| Situation | Result |
|---|---|
| The backend serves no OpenVPN endpoints for the location | Returns [] — this is not an error and does not throw |
| The backend serves several | Returned 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 know | That 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.
| Property | Type | Description |
|---|---|---|
username | String | The auth-user-pass username |
password | String | The auth-user-pass password |
These are per-license VPN credentials — not the account's login credentials, not interchangeable with them.
| Situation | Result |
|---|---|
| The active subscription's license carries OpenVPN credentials | Returns them |
| No license matches the active subscription | Returns nil |
| No subscription has been activated yet | Returns 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:
| Call | Throws |
|---|---|
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