Skip to main content

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.

CasePassword 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")
info

.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:

MethodBehaviour
.offNo verification
.automaticVerified against the system trust store (default)
.manualVerified 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 protocolBehaviour with a proxy set
.lightwayTCP 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
.automaticEvery 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, .openvpnNo 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)
info

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:

DataStored in
Server, port, cert validation method, credential kind, username, resolved IPsKapeSharedState (JSON in the App Group container), so the Network Extension can read it
Static password, PEM certificateShared keychain (keychainService + keychainAccessGroup from KapeSDK.plist)
.tokenAuth passwordNot 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]