HTTPS Proxy
The SDK can route the VPN tunnel through one or more client-supplied HTTPS proxies. The proxy carries the Lightway-over-TCP handshake and tunnel: the proxy is applied to Lightway TCP endpoints only. WireGuard and OpenVPN endpoints are never proxied.
Proxies are supplied entirely by your app — the SDK has no built-in proxy list and no
region-specific behaviour. Configuring one is a two-step flow: persist the proxies with
setProxyConfig, then connect.
Setting a Proxy
val vpn = KapeVpnManager(
handle,
protocols = listOf(KapeWireGuardSupport, KapeLightwaySupport, KapeOpenVpnSupport),
)
vpn.setProxyConfig(
KapeProxyConfig(
server = "proxy.example.com",
port = 443,
credential = KapeProxyCredential.UsernamePassword("user", "secret"),
certValidationMethod = KapeProxyCertValidationMethod.Automatic,
),
)
setProxyConfig is suspend: it waits for the license verdict, refuses with
KapeVpnSettingResult.NotLicensed if VpnHttpsProxy is not granted, and otherwise updates the
exposed flows and persists before returning. The selection is written to the shared DataStore and
read by KapeVpnService on its next startup. Changes apply to the next VPN connection — reconnect to pick up a change
mid-session:
vpn.setProxyConfig(config)
if (vpn.connectionStatus.value != KapeVPNConnectionStatus.Disconnected) {
vpn.disconnect()
}
vpn.setSelectedProtocol(KapeVPNProtocol.Lightway)
vpn.connect(KapeConnectionMode.Auto)
Clearing a Proxy
vpn.setProxyConfig(null) // or setProxyConfig(emptyList())
Clearing removes the persisted entry, including the stored secrets.
Credentials
Authentication is modelled by KapeProxyCredential. The username is always a plain string
supplied by your app; only the password may be token-sourced.
| Case | Password source |
|---|---|
KapeProxyCredential.UsernamePassword(username, password) | Static, supplied by your app and persisted encrypted |
KapeProxyCredential.TokenAuth(username) | The Kape Client SDK connection token, resolved fresh at every connect — never persisted |
// Static credentials
KapeProxyCredential.UsernamePassword("user", "secret")
// Password is the connection token, refreshed on every connection
KapeProxyCredential.TokenAuth("user")
TokenAuth needs a valid connection token at connect time. If the token cannot be fetched,
Lightway endpoints are skipped and the connection falls through to the remaining endpoints.
Certificate Validation
certValidationMethod controls how the proxy's TLS certificate is verified:
| Method | Behaviour |
|---|---|
Off | No verification |
Automatic | Verified against the system trust store (default) |
Manual | Verified against the PEM supplied in certificate |
certificate is sent only when the method is Manual; it is ignored otherwise.
vpn.setProxyConfig(
KapeProxyConfig(
server = "proxy.example.com",
port = 443,
credential = KapeProxyCredential.TokenAuth("user"),
certValidationMethod = KapeProxyCertValidationMethod.Manual,
certificate = context.assets.open("proxy_ca.pem").reader().readText(),
),
)
Interaction With Protocol and Transport
The proxy applies only to Lightway TCP endpoints, so the selected protocol decides what happens to the rest of the endpoint list:
| Selected protocol | Behaviour with a proxy set |
|---|---|
KapeVPNProtocol.Lightway | TCP is forced — UDP Lightway endpoints are dropped so the proxy is never silently bypassed. Strict: if no TCP endpoint exists, the connection has nothing to attempt |
KapeVPNProtocol.Automatic | Every endpoint is kept in the Low Level SDK's pecking order; the proxy is applied in place on the Lightway TCP ones. Pass promoteUnderAutomatic = true to lead with the proxied Lightway TCP endpoints instead |
KapeVPNProtocol.Wireguard, KapeVPNProtocol.OpenVPN | No effect — those endpoints are never proxied |
Under Lightway, the forced TCP filter outranks vpn.transport — a KapeVPNTransport.UDP
selection is discarded rather than silently bypassing the proxy. Under Automatic the transport
selection is ignored anyway. See
Network Transport.
// Strict: Lightway over TCP through the proxy, or nothing.
vpn.setSelectedProtocol(KapeVPNProtocol.Lightway)
vpn.setProxyConfig(config)
// Try proxied Lightway TCP first, then fall back through the normal pecking order.
vpn.setSelectedProtocol(KapeVPNProtocol.Automatic)
vpn.setProxyConfig(listOf(config), promoteUnderAutomatic = true)
The single-proxy convenience setProxyConfig(config) always uses promoteUnderAutomatic = false.
Use the list form when you need to set the flag.
Multiple Proxies
Passing more than one proxy spreads them round-robin across the Lightway TCP endpoints — the first proxied endpoint uses the first proxy, the second uses the second, wrapping as needed. All proxies in the list are used within a single connection attempt sequence.
vpn.setProxyConfig(
listOf(
KapeProxyConfig("proxy-a.example.com", 443, KapeProxyCredential.TokenAuth("user")),
KapeProxyConfig("proxy-b.example.com", 443, KapeProxyCredential.TokenAuth("user")),
),
)
Pre-resolved Proxy IPs
Set resolvedIps to IP literals (IPv4 or IPv6) for server, and Lightway connects to those
addresses instead of looking server up in DNS. That keeps the proxy reachable where its DNS name
is blocked or poisoned.
Each IP is its own round-robin entry: a proxy with two IPs counts as two entries, with the same
host, credential and certificate. An empty list (the default) means Lightway resolves server
itself. The SDK does not validate the strings. A value that is not an IP fails that endpoint at
connect time.
vpn.setProxyConfig(
listOf(
KapeProxyConfig(
server = "proxy.example.com",
port = 443,
credential = KapeProxyCredential.TokenAuth("user"),
resolvedIps = listOf("203.0.113.10", "203.0.113.11"),
),
),
)
Reading the Configured Proxies
// Snapshot
val configs = vpn.proxyConfigs() // emptyList() when none configured
val promotes = vpn.promoteProxyUnderAutomaticFlow.value
// Observe (e.g. to drive a settings screen)
viewModelScope.launch {
vpn.proxyConfigsFlow.collect { println(it) }
}
proxyConfigsFlow is loaded from the DataStore asynchronously when KapeVpnManager is
constructed, so it may briefly report emptyList() at startup. It exists to feed your UI — the
tunnel re-reads the persisted value itself on every connect, so a connection is never affected by
the flow's load timing.
Storage
The whole proxy list — credentials and PEM certificate included — is serialised to JSON,
encrypted with AES/GCM using an Android Keystore-backed key (alias kape_proxy_config_key), and
stored Base64-encoded in the SDK's DataStore. A TokenAuth proxy stores no password; its
connection token is fetched at every connect.
Limitations
- Lightway TCP only. WireGuard and OpenVPN endpoints ignore the proxy entirely.
- Not applied to Anonymous Dedicated IP. An Anonymous Dedicated IP connection resolves a single Lightway endpoint through its own path; configured proxies are ignored and a warning is logged. A Simple Dedicated IP uses the proxy like any location.
- Applies on the next connection. An active tunnel keeps its current proxy state until it reconnects.
API Reference
KapeProxyConfig
data class KapeProxyConfig(
val server: String,
val port: Int,
val credential: KapeProxyCredential,
val certValidationMethod: KapeProxyCertValidationMethod = KapeProxyCertValidationMethod.Automatic,
/** PEM certificate used only when [certValidationMethod] is [KapeProxyCertValidationMethod.Manual]. */
val certificate: String = "",
/** Pre-resolved IP literals for [server]; each is its own round-robin entry. */
val resolvedIps: List<String> = emptyList(),
)
KapeProxyCredential
sealed class KapeProxyCredential {
data class UsernamePassword(
val username: String,
val password: String,
) : KapeProxyCredential()
/** Password is resolved at connect time from the connection token — never persisted. */
data class TokenAuth(
val username: String,
) : KapeProxyCredential()
}
KapeProxyCertValidationMethod
enum class KapeProxyCertValidationMethod {
Off,
Automatic,
Manual,
}
KapeVpnManager
/** Sets one or more HTTPS proxies. Empty list clears. Applies on next connection. */
suspend fun setProxyConfig(configs: List<KapeProxyConfig>, promoteUnderAutomatic: Boolean = false): KapeVpnSettingResult
/** Single-proxy convenience forwarding to the list form. `null` clears. */
suspend fun setProxyConfig(config: KapeProxyConfig?): KapeVpnSettingResult
/** Current snapshot of the configured proxies. */
fun proxyConfigs(): List<KapeProxyConfig>
/** Configured HTTPS proxies (Lightway-over-TCP only). Empty when none are set. */
val proxyConfigsFlow: StateFlow<List<KapeProxyConfig>>
/** Whether proxied Lightway-TCP endpoints lead under Automatic. */
val promoteProxyUnderAutomaticFlow: StateFlow<Boolean>