OpenVPN Endpoint Discovery
KapeVPNDiscoveryService returns the full OpenVPN payload the backend serves for a location — enough
to write an .ovpn file — plus the OpenVPN credentials that go with it, without running the SDK's
tunnel.
This is the API to reach for when your product hands OpenVPN configuration to something else: your
own backend, a third-party OpenVPN client, or a file on disk. It ships as its own artifact,
kape-vpn-discovery, which depends on kape-core alone. An app module that depends only on it
links no WireGuard, OpenVPN or Lightway native library, so none of them land in your APK.
Quick start
import com.kape.platformsdk.vpn.discovery.FetchOpenVPNEndpointsResult
import com.kape.platformsdk.vpn.discovery.FetchProtocolLocationsResult
import com.kape.platformsdk.vpn.discovery.KapeVPNDiscoveryService
import com.kape.platformsdk.vpn.discovery.models.KapeVPNProtocol
val discovery = KapeVPNDiscoveryService(handle)
// 1. Which locations serve OpenVPN?
val locations =
when (val result = discovery.fetchLocations(KapeVPNProtocol.OpenVPN)) {
is FetchProtocolLocationsResult.Success -> result.locations
is FetchProtocolLocationsResult.SdkError -> return handleError(result.error)
}
// 2. What does one of them serve?
when (val result = discovery.fetchOpenVPNEndpoints(locations.first().id)) {
is FetchOpenVPNEndpointsResult.Success ->
result.endpoints.forEach { endpoint ->
// endpoint.ovpnConfiguration is the server-supplied .ovpn contents
writeConfig(endpoint.ovpnConfiguration)
}
is FetchOpenVPNEndpointsResult.SdkError -> handleError(result.error)
}
Requires an activated subscription — the active subscription is read from the KapeSdkHandle on
every call. See Getting Started.
The protocol is always explicit
Both calls take a KapeVPNProtocol. KapeLocationService instead resolves it through a provider
you supply at construction, which for KapeVPN consumers is the persisted selection. This service
needs no provider, so it works in a process that has never written one — which matters, because a
product that only generates config files never runs the tunnel that would have written it.
KapeVPNProtocol.Automatic asks for every protocol the SDK would attempt.
KapeOpenVPNEndpoint
A faithful passthrough of the backend's payload, not a normalized model.
| Member | Type | Meaning |
|---|---|---|
host | String | The endpoint address — an IPv4 or IPv6 literal |
port | Int | The endpoint port |
transport | KapeVPNEndpointTransport | TCP or UDP. An endpoint always names one |
mtu | Int | Server-supplied MTU. 0 when the server did not specify one |
certDn | String | Distinguished Name the server certificate should present |
ovpnConfiguration | String | The server-supplied .ovpn file contents |
xorValue | Long? | XOR obfuscation scramble value, or null when the server sent none |
hasXorObfuscation | Boolean | Whether obfuscation applies. false for both null and 0 |
Two things about these members that are easy to get wrong:
ovpnConfiguration is authoritative. Directives such as cipher and auth appear only
there, so the config cannot be reconstructed from the other members. Use it as the base you adapt
for whichever client you target. The scalars exist so you can list, filter or pick an endpoint
without parsing config text — they are selection metadata, not a second copy of the config. Where
mtu and the config's own tun-mtu disagree, tun-mtu wins.
hasXorObfuscation is not xorValue != null. The backend uses 0 to mean "no obfuscation", so
a 0 and an absent value mean the same thing. The raw value is preserved as the server sent it and
the interpretation lives in hasXorObfuscation — use that rather than comparing xorValue yourself.
xorValue carries the backend's unsigned 64-bit field as the Long with the same bit pattern, so a
very large value reads as negative (ULong.MAX_VALUE is -1L). No information is lost; call
toULong() if you need the unsigned value.
The Low Level SDK's ovpnConfigTemplate is deliberately not exposed: it is redundant against
ovpnConfiguration, which is what a tunnel actually consumes.
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:
import com.kape.platformsdk.vpn.discovery.FetchOpenVPNCredentialsResult
when (val result = discovery.fetchOpenVPNCredentials()) {
is FetchOpenVPNCredentialsResult.Success ->
result.credentials?.let { connect(it.username, it.password) }
?: showNoCredentialsProvisioned()
is FetchOpenVPNCredentialsResult.SdkError -> handleError(result.error)
}
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.
| Member | Type | Meaning |
|---|---|---|
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, and not interchangeable with them.
Success.credentials is nullable, and a null is an answer rather than a failure: no license matches
the active subscription, the matching license has no OpenVPN credentials provisioned, or no
subscription is active yet. Only a genuine SDK or network failure produces SdkError.
To connect the SDK's own OpenVPN tunnel with them, hand them to
KapeVpnManager.saveOpenVpnCredentials(username, password) — the setter the next connect reads.
Fetching does not save.
Empty is a success
A location the backend serves no OpenVPN endpoints for returns Success with an empty list, not an
error. The same holds for fetchLocations when no location serves the requested protocol. Only a
genuine SDK or network failure produces SdkError.
Endpoints come back in the Low Level SDK's attempt order — the order the tunnel itself would try them — so the first entry is the one to prefer.
Depending on discovery alone
If your app needs endpoint configuration but not the tunnel, depend on the discovery artifact
directly instead of the fused kape-platform-sdk:
dependencies {
implementation("com.kape.platformsdk:kape-vpn-discovery:<version>")
}
You can confirm nothing tunnel-related came along:
./gradlew :your-app:dependencies --configuration releaseRuntimeClasspath
Neither com.kape.amneziawg:wireguard-tunnel, com.github.pia-foss.mobile-android-vpn-manager:openvpn
nor com.kape:lightway should appear.
Listing endpoints across all protocols
KapeVPNDiscoveryService is OpenVPN-specific. To see what a location serves across every protocol —
useful for diagnostics rather than for connecting — use KapeLocationService.fetchEndpoints, which
returns the lighter KapeVPNEndpointInfo (host, port, protocolName):
val result = locationService.fetchEndpoints(locationId, KapeVPNProtocol.Automatic)
protocolName is one of "wireguard", "openvpn", "lightway" or "httpproxy". That model is
deliberately not enough to connect with; the two answer different questions.
Related
- Server Locations — the stateful
KapeLocationServiceand itsStateFlows - VPN Protocol — selecting the protocol the tunnel uses
- Installation — artifacts and coordinates