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. It requires the VpnHttpsProxy feature on your SDK license — see
Licensed VPN features.
Setting a Proxy
let vpn = KapeVPNManager(handle: handle)
try vpn.setProxyConfig(
KapeProxyConfig(
server: "proxy.example.com",
port: 443,
credential: .usernamePassword(username: "user", password: "secret"),
certValidationMethod: .automatic
)
)
The selection is persisted and read by the Network Extension on its next startup. Changes apply to the next VPN connection — reconnect to pick up a change mid-session:
try vpn.setProxyConfig(config)
if vpn.connectionStatus != .disconnected {
await vpn.disconnect()
}
try vpn.selectVpnProtocol(.lightway)
try await vpn.connect(mode: .auto)
Clearing a Proxy
try vpn.setProxyConfig(nil) // or setProxyConfig([])
Clearing removes both the persisted metadata and 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 |
|---|---|
.usernamePassword(username:password:) | Static, supplied by your app and stored in the keychain |
.tokenAuth(username:) | The Kape Client SDK connection token, resolved fresh at every connect — never persisted |
// Static credentials
.usernamePassword(username: "user", password: "secret")
// Password is the connection token, refreshed on every connection
.tokenAuth(username: "user")
.tokenAuth needs a valid connection token at connect time. If the token cannot be fetched, the
extension logs proxy token unavailable — proxy cannot authenticate. With an explicit .lightway
selection the connection fails; under .automatic the default Lightway authentication, which uses
the same token, fails too, so the Lightway endpoints are skipped and the remaining endpoints can
still be attempted.
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.
try vpn.setProxyConfig(
KapeProxyConfig(
server: "proxy.example.com",
port: 443,
credential: .tokenAuth(username: "user"),
certValidationMethod: .manual,
certificate: try String(contentsOfFile: pemPath, encoding: .utf8)
)
)
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 |
|---|---|
.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 |
.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 |
.wireguard, .openvpn | No effect — those endpoints are never proxied |
Under .lightway, the forced TCP filter outranks selectTransport(_:) — a .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.
try vpn.selectVpnProtocol(.lightway)
try vpn.setProxyConfig(config)
// Try proxied Lightway TCP first, then fall back through the normal pecking order.
try vpn.selectVpnProtocol(.automatic)
try vpn.setProxyConfig([config], promoteUnderAutomatic: true)
The single-proxy convenience setProxyConfig(_:) 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.
try vpn.setProxyConfig([
KapeProxyConfig(server: "proxy-a.example.com", port: 443, credential: .tokenAuth(username: "user")),
KapeProxyConfig(server: "proxy-b.example.com", port: 443, credential: .tokenAuth(username: "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.
try vpn.setProxyConfig([
KapeProxyConfig(
server: "proxy.example.com", port: 443,
credential: .tokenAuth(username: "user"),
resolvedIps: ["203.0.113.10", "203.0.113.11"]
),
])
Reading the Configured Proxies
let configs = try vpn.proxyConfigs() // [] when none configured
Static passwords and certificates are read back from the keychain. For a .tokenAuth proxy the
password is left empty — it is resolved at connect time, not stored.
Storage
Proxy data is split by sensitivity:
| Data | Stored in |
|---|---|
| Server, port, cert validation method, credential kind, username, resolved IPs | KapeSharedState (JSON in the App Group container), so the Network Extension can read it |
| Static password, PEM certificate | Shared keychain (keychainService + keychainAccessGroup from KapeSDK.plist) |
.tokenAuth password | Not stored — the 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 and does not use the configured proxies. A Simple Dedicated IP uses them like any other location.
- Applies on the next connection. An active tunnel keeps its current proxy state until it reconnects.
API Reference
KapeProxyConfig
public struct KapeProxyConfig: Codable, Sendable, Equatable {
public var server: String
public var port: UInt16
public var credential: KapeProxyCredential
public var certValidationMethod: KapeProxyCertValidationMethod
/// PEM cert; sent only when `certValidationMethod == .manual`.
public var certificate: String
/// Pre-resolved IP literals for `server`; each is its own round-robin entry.
public var resolvedIps: [String]
public init(
server: String,
port: UInt16,
credential: KapeProxyCredential = .usernamePassword(username: "", password: ""),
certValidationMethod: KapeProxyCertValidationMethod = .automatic,
certificate: String = "",
resolvedIps: [String] = []
)
}
KapeProxyCredential
public enum KapeProxyCredential: Codable, Sendable, Equatable {
/// Static credentials supplied by the client.
case usernamePassword(username: String, password: String)
/// Password is the connection token, resolved fresh at connect.
case tokenAuth(username: String)
public var username: String { get }
}
KapeProxyCertValidationMethod
public enum KapeProxyCertValidationMethod: String, Codable, Sendable, CaseIterable {
case off
case automatic
case manual
}
KapeVPNManager
/// Sets one or more HTTPS proxies. An empty list clears. Applies on next connection.
public func setProxyConfig(_ configs: [KapeProxyConfig], promoteUnderAutomatic: Bool = false) throws
/// Single-proxy convenience forwarding to the list form. `nil` clears.
public func setProxyConfig(_ config: KapeProxyConfig?) throws
/// Returns the persisted HTTPS proxies (secrets read from the keychain).
public func proxyConfigs() throws -> [KapeProxyConfig]