Skip to content
Open
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
03f5f91
Add kape-platform-sdk pull script, pinned version, gitignore and reso…
kp-diego-trevisan Jun 12, 2026
a686d37
Migrate PIALibrary OpenVPN integration to Kape TunnelKit fork API
kp-diego-trevisan Jun 12, 2026
903d4b1
Add PlatformSDK-Tunnel network extension target
kp-diego-trevisan Jun 12, 2026
b7d6521
Add KapePlatformSDKTunnelProfile and set extension bundle id to Platf…
kp-diego-trevisan Jun 15, 2026
53229cf
Route VPN through PlatformSDK profile behind usePlatformSDKVPN featur…
kp-diego-trevisan Jun 15, 2026
997aaaf
Wire kill switch and on-demand rules for the PlatformSDK tunnel
kp-diego-trevisan Jun 15, 2026
21d60b9
PlatformSDK tunnel: file-based shared state for location, servers and…
kp-diego-trevisan Jun 15, 2026
f25e086
Create PIAVPN local package and organize tunnel adapters under Tunnel/
kp-diego-trevisan Jun 16, 2026
5564034
Update KapePlatformSDK to 1.0.0-ec946f2 and add --update flag to pull…
kp-diego-trevisan Jun 17, 2026
dcc7b10
Integrate OpenVPN into the PlatformSDK tunnel (cipher, auth, port, MT…
kp-diego-trevisan Jun 18, 2026
977782c
CI: pull KapePlatformSDK before running tests, with caching
kp-diego-trevisan Jun 18, 2026
ac2a4bd
PlatformSDK: integrate OpenVPN settings, fix wifi/cellular roaming, b…
kp-diego-trevisan Jun 19, 2026
febbea6
PlatformSDK: drive NE reasserting flag during mid-session reconnects
kp-diego-trevisan Jun 19, 2026
c51a1d4
PlatformSDK: integrate Dedicated IP (DIP) servers and fix WireGuard DNS
kp-diego-trevisan Jun 22, 2026
1094427
PlatformSDK: pin WireGuard key-exchange TLS to bundled PIA root CA
kp-diego-trevisan Jun 22, 2026
7626739
CI: pull KapePlatformSDK during Xcode Cloud post-clone
kp-diego-trevisan Jun 22, 2026
9fe6bf8
PlatformSDK: fix extension bundle id and version stamping for App Store
kp-diego-trevisan Jun 22, 2026
064dc5e
PlatformSDK: prepare for tvOS support and harden tunnel restart state…
kp-diego-trevisan Jun 23, 2026
b8e8397
Fix forced usePlatformSDKVPN flag to iOS only
kp-diego-trevisan Jun 23, 2026
6ddaf48
Remove stale PIA tvOS Tunnel
kp-diego-trevisan Jun 23, 2026
e640cdb
Add PlatformSDK tunnel, protocol selection, and fix app group sandbox…
kp-diego-trevisan Jun 24, 2026
921c336
Clear shared state file on logout and add SecTrustSetAnchorCertificat…
kp-diego-trevisan Jun 24, 2026
3a760f7
PlatformSDK: apply user custom DNS to the tunnel and centralize VPN t…
kp-diego-trevisan Jun 25, 2026
abe8509
PlatformSDK: add integration ADR, fix async legacy profile cleanup, a…
kp-diego-trevisan Jun 25, 2026
7511fec
PlatformSDK: let the tunnel extension autonomously fetch the server l…
kp-diego-trevisan Jun 26, 2026
a4c6e54
PlatformSDK: conform PIAWireguardAuthenticator to the vendored SDK's …
kp-diego-trevisan Jun 26, 2026
4481e55
PlatformSDK: rank tunnel endpoints by latency, carry DIP server expli…
kp-diego-trevisan Jun 29, 2026
17d99f2
PlatformSDK: unify protocol selection on KapePlatformSDKVPNType and r…
kp-diego-trevisan Jun 30, 2026
fbe4c1a
Bump kape-platform-sdk.version
kp-diego-trevisan Jul 1, 2026
1a10bf7
PlatformSDK: report the tunnel's actual resolved protocol/server/tran…
kp-diego-trevisan Jul 1, 2026
1cb4e23
PlatformSDK: switch the active tunnel's location in place via shared-…
kp-diego-trevisan Jul 2, 2026
77630f5
PlatformSDK: drive Connecting UI from the tunnel's reported status vi…
kp-diego-trevisan Jul 3, 2026
567d76e
CI: prefetch the OpenSSL binary xcframework via curl and seed it into…
kp-diego-trevisan Jul 3, 2026
b8ea970
Fix tvOS unit tests: restore PIA VPN-tvOS product name on Development…
kp-diego-trevisan Jul 3, 2026
599874f
Fix tvOS connection state priority
kp-diego-trevisan Jul 3, 2026
99b8d15
PlatformSDK: fix fresh-install default protocol and localize Automati…
kp-diego-trevisan Jul 3, 2026
8014c0f
PlatformSDK: derive legacy tunnel extension bundle ids from the app b…
kp-diego-trevisan Jul 6, 2026
2dbe48d
PlatformSDK: add arm64 to UIRequiredDeviceCapabilities for tvOS tunne…
kp-diego-trevisan Jul 6, 2026
512dbb4
PlatformSDK: seed initial VPN status and connection timestamp when ad…
kp-diego-trevisan Jul 7, 2026
64975f4
Document Kape Platform SDK setup in README and bump pinned SDK version
kp-diego-trevisan Jul 7, 2026
80ea5d9
PlatformSDK: derive Development tunnel extension bundle id from app b…
kp-diego-trevisan Jul 7, 2026
916b07a
Verify cached Kape Platform SDK archives
kp-diego-trevisan Jul 13, 2026
ba29e8f
Add automatic-protocol pecking order with dictated OpenVPN crypto, tr…
kp-diego-trevisan Jul 17, 2026
e99bb0c
Wire up PlatformSDK tunnel data usage over IPC
kp-diego-trevisan Jul 21, 2026
fe2f902
Fix build after rebase onto master
kp-diego-trevisan Jul 21, 2026
57b33ee
PlatformSDK: suppress PIA-level fallback reconnect timer while connec…
kp-diego-trevisan Jul 21, 2026
eae4811
PlatformSDK: implement VPNProfile.connectionDate on KapePlatformSDKTu…
kp-diego-trevisan Jul 21, 2026
ef641fd
Fix tvOS bundle ID and update VPN integration docs
kp-diego-trevisan Jul 21, 2026
d2c4b95
Fix Mac Catalyst archive: exclude iOS-only PlatformSDK tunnel via pla…
kp-diego-trevisan Jul 21, 2026
923c4dd
PlatformSDK: address PR review feedback on VPN integration
kp-diego-trevisan Jul 23, 2026
211e8a9
Merge branch 'master' into kape-platform-sdk-integration
kp-diego-trevisan Jul 28, 2026
d9020d8
Merge branch 'master' into kape-platform-sdk-integration
kp-diego-trevisan Jul 29, 2026
81a0929
Merge branch 'master' into kape-platform-sdk-integration
kp-diego-trevisan Jul 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/ios_pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,37 @@ jobs:
echo "Failed to download iOS platform after $max_attempts attempts"
exit 1

# Cache the downloaded archive, not the unpacked package: the pull script
# re-verifies the archive's SHA-256 against the committed pin on every run,
Comment thread
kp-diego-trevisan marked this conversation as resolved.
Comment thread
kp-diego-trevisan marked this conversation as resolved.
Comment thread
kp-diego-trevisan marked this conversation as resolved.
Comment thread
kp-diego-trevisan marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM · SECURITY_REVIEW — KapePlatformSDK supply-chain integrity check skipped on GitHub Actions cache hits and developer workstations

scripts/pull-kape-platform-sdk.sh contains a checksum verification step that compares the downloaded archive against the SHA-256 published in the registry's release metadata. However, two code paths bypass this check: Gap 1 — GitHub Actions cache-hit short-circuits the pull script before checksum logic runs. Both .github/workflows/ios_pull_request.yml and .github/workflows/tvos_pull_request.yml restore LocalPackages/KapePlatformSDK/ from the Actions cache before running `pull-kape-…

View finding in KB

kb · F-9049621E2Y5440GN

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM · SECURITY_REVIEW — KapePlatformSDK supply-chain integrity check skipped on GitHub Actions cache hits and developer workstations

scripts/pull-kape-platform-sdk.sh contains a checksum verification step that compares the downloaded archive against the SHA-256 published in the registry's release metadata. However, two code paths bypass this check: Gap 1 — GitHub Actions cache-hit short-circuits the pull script before checksum logic runs. Both .github/workflows/ios_pull_request.yml and .github/workflows/tvos_pull_request.yml restore LocalPackages/KapePlatformSDK/ from the Actions cache before running `pull-kape-…

View finding in KB

kb · F-9049621E2Y5440GN

# so a poisoned cache entry cannot reach the build unnoticed.
- name: Cache KapePlatformSDK archive
uses: actions/cache@v4
Comment thread
kp-diego-trevisan marked this conversation as resolved.
Comment thread
kp-diego-trevisan marked this conversation as resolved.
with:
path: .kape-sdk-archive-cache
key: kape-platform-sdk-archive-${{ hashFiles('scripts/kape-platform-sdk.version', 'scripts/kape-platform-sdk.checksum') }}

- name: Pull KapePlatformSDK
env:
CLOUDSMITH_TOKEN: ${{ secrets.CLOUDSMITH_TOKEN }}
KAPE_SDK_ARCHIVE_CACHE: .kape-sdk-archive-cache
run: ./scripts/pull-kape-platform-sdk.sh

- name: Cache SPM packages
uses: actions/cache@v4
with:
path: .build/SourcePackages
key: spm-${{ runner.os }}-${{ hashFiles('**/Package.resolved') }}
restore-keys: spm-${{ runner.os }}-

- name: Set up Ruby

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM · SECURITY_REVIEW — New PR CI workflows use external GitHub Actions pinned to mutable version tags with access to org credentials

The new ios_pull_request.yml and tvos_pull_request.yml CI workflows use three external GitHub Actions pinned to mutable version tags instead of full commit SHAs: - maxim-lobanov/setup-xcode@v1 - nick-fields/retry@v4 - ruby/setup-ruby@v1 Per the repository advisory guidance, only actions from the xvpn, xvpn-meta, and xvpn-sec GitHub organisations are permitted to use mutable labels; all other external actions remain untrusted. None of these three actions come from those organisat…

View finding in KB

kb · SEC-M-4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM · SECURITY_REVIEW — New PR CI workflows use external GitHub Actions pinned to mutable version tags with access to org credentials

The new ios_pull_request.yml and tvos_pull_request.yml CI workflows use three external GitHub Actions pinned to mutable version tags instead of full commit SHAs: - maxim-lobanov/setup-xcode@v1 - nick-fields/retry@v4 - ruby/setup-ruby@v1 Per the repository advisory guidance, only actions from the xvpn, xvpn-meta, and xvpn-sec GitHub organisations are permitted to use mutable labels; all other external actions remain untrusted. None of these three actions come from those organisat…

View finding in KB

kb · SEC-M-4

uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3.5'
bundler-cache: true

- name: Prefetch OpenSSL binary artifact
run: ./scripts/prefetch-openssl-artifact.sh .build/SourcePackages

- name: Run iOS unit tests
uses: nick-fields/retry@v4
with:
Comment thread
kp-diego-trevisan marked this conversation as resolved.
Comment thread
kp-diego-trevisan marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOW · SECURITY_REVIEW — New PR CI workflows use external GitHub Actions pinned to mutable version tags

The newly added .github/workflows/ios_pull_request.yml and .github/workflows/tvos_pull_request.yml use external GitHub Actions pinned to mutable major-version tags rather than immutable commit SHAs: - maxim-lobanov/setup-xcode@v1 — third-party, not GitHub-maintained - nick-fields/retry@v4 — third-party, not GitHub-maintained - ruby/setup-ruby@v1 — Ruby organization - actions/cache@v4, actions/checkout@v6, actions/upload-artifact@v6 — GitHub-maintained but not SHA-pinned Per the…

View finding in KB

kb · F-WK64N10YSZ6K7VVN

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOW · SECURITY_REVIEW — New PR CI workflows use external GitHub Actions pinned to mutable version tags

The newly added .github/workflows/ios_pull_request.yml and .github/workflows/tvos_pull_request.yml use external GitHub Actions pinned to mutable major-version tags rather than immutable commit SHAs: - maxim-lobanov/setup-xcode@v1 — third-party, not GitHub-maintained - nick-fields/retry@v4 — third-party, not GitHub-maintained - ruby/setup-ruby@v1 — Ruby organization - actions/cache@v4, actions/checkout@v6, actions/upload-artifact@v6 — GitHub-maintained but not SHA-pinned Per the…

View finding in KB

kb · F-WK64N10YSZ6K7VVN

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOW · SECURITY_REVIEW — New PR CI workflows use external GitHub Actions pinned to mutable version tags

The newly added .github/workflows/ios_pull_request.yml and .github/workflows/tvos_pull_request.yml use external GitHub Actions pinned to mutable major-version tags rather than immutable commit SHAs: - maxim-lobanov/setup-xcode@v1 — third-party, not GitHub-maintained - nick-fields/retry@v4 — third-party, not GitHub-maintained - ruby/setup-ruby@v1 — Ruby organization - actions/cache@v4, actions/checkout@v6, actions/upload-artifact@v6 — GitHub-maintained but not SHA-pinned Per the…

View finding in KB

kb · F-WK64N10YSZ6K7VVN

Expand Down
25 changes: 25 additions & 0 deletions .github/workflows/tvos_pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,37 @@ jobs:
echo "Failed to download tvOS platform after $max_attempts attempts"
exit 1

# Cache the downloaded archive, not the unpacked package: the pull script
# re-verifies the archive's SHA-256 against the committed pin on every run,
# so a poisoned cache entry cannot reach the build unnoticed.
- name: Cache KapePlatformSDK archive
uses: actions/cache@v4
with:
path: .kape-sdk-archive-cache
key: kape-platform-sdk-archive-${{ hashFiles('scripts/kape-platform-sdk.version', 'scripts/kape-platform-sdk.checksum') }}

- name: Pull KapePlatformSDK
env:
CLOUDSMITH_TOKEN: ${{ secrets.CLOUDSMITH_TOKEN }}
KAPE_SDK_ARCHIVE_CACHE: .kape-sdk-archive-cache
run: ./scripts/pull-kape-platform-sdk.sh

- name: Cache SPM packages
uses: actions/cache@v4
with:
path: .build/SourcePackages
key: spm-${{ runner.os }}-${{ hashFiles('**/Package.resolved') }}
restore-keys: spm-${{ runner.os }}-

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3.5'
bundler-cache: true

- name: Prefetch OpenSSL binary artifact
run: ./scripts/prefetch-openssl-artifact.sh .build/SourcePackages

- name: Run tvOS unit tests
uses: nick-fields/retry@v4
with:
Expand Down
12 changes: 11 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
.DS_Store
*.swp
*.pbxuser
**/xcuserdata
**/*.xcworkspace/xcuserdata
**/*.xcodeproj/project.xcworkspace
**/*.xcodeproj/xcuserdata
Pods
fastlane/**/*.html
fastlane/README.md
Expand All @@ -16,3 +18,11 @@ Gemfile.lock
.build
.swiftpm
LocalPackages/**/Package.resolved

# KapePlatformSDK is pulled from the Cloudsmith registry by
# scripts/pull-kape-platform-sdk.sh — never committed.
/LocalPackages/KapePlatformSDK/
# Archive cache used by CI ($KAPE_SDK_ARCHIVE_CACHE).
/.kape-sdk-archive-cache/
# Cloudsmith entitlement token used by the pull script (a secret).
.cloudsmith
140 changes: 140 additions & 0 deletions ADRs/0005-integrate-kape-platform-sdk-vpn-engine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# 0005: Integrate the Kape Platform SDK VPN engine

Date: 2026-06-25

## Context

PIA historically ships three independent VPN protocol stacks, each as its own Network
Extension target with its own connection, crypto, and reconnection logic:

- **IKEv2** — native `NEVPNProtocolIKEv2`.
- **OpenVPN** — a TunnelKit fork via `mobile-ios-openvpn`.
- **WireGuard** — WireGuard-Go via `mobile-ios-wireguard`.

Maintaining three divergent tunnel implementations is expensive: every protocol fix,
reconnection improvement, or path-monitoring change has to be made (and tested) up to three
times, and the implementations have drifted in behaviour over time.

Kape (the parent organisation) provides the **Kape Platform SDK** (`kp_platform_sdks`):
platform-native Swift/Kotlin SDKs layered on top of the Rust-based Kape Client SDK. The Apple
side (`apple/KapePlatformSDK`) exposes a packet-tunnel engine that already implements OpenVPN
and WireGuard (and Lightway) on a single shared core, with reconnection and network-path
handling built in. Consolidating PIA's VPN engine onto this core lets PIA share one maintained
implementation with the rest of Kape's brands instead of carrying its own.

## Decision

We integrate **only the Platform SDK's VPN packet-tunnel engine** — not its account, auth,
subscription, or UI layers. PIA keeps its existing `AccountProvider`, `ServerProvider`,
regions, CSI, KPI, and UI; only the tunnel implementation is swapped.

**Dependency model — vendored, pinned, checksum-verified, pulled in CI.** The SDK is not a
normal SPM dependency. It is pulled from a private Cloudsmith registry by
`scripts/pull-kape-platform-sdk.sh` into `LocalPackages/KapePlatformSDK/` (gitignored). Two pins
guard it: the version (a semver with a source-commit suffix, e.g. `1.0.0-99f3be8`) in
`scripts/kape-platform-sdk.version`, and the archive's SHA-256 in
`scripts/kape-platform-sdk.checksum`. The pull is **fail-closed on integrity**: the script
verifies the downloaded archive's SHA-256 against the committed checksum on every run — including
cached archives — and never installs an archive that lacks or fails its checksum. CI caches the
*archive* (not the unpacked package) precisely so this verification re-runs each time.
`ci_scripts/ci_post_clone.sh` and the CI workflows run the pull (and cache it) before resolving
SPM. Imported products:

- `KapeVPN-PacketTunnel` — the tunnel engine, WireGuard controller, and internal reconnection.
- `KapeVPN-OpenVPN` — the OpenVPN connection controller.
- `TunnelKitPackage` (the Kape TunnelKit fork) — consumed by `PIALibrary` for OpenVPN config types.

**Adapter layer — new `LocalPackages/PIAVPN/` package.** PIA bridges its own data model onto
the SDK's interface protocols with thin adapters that run inside the extension:

| PIA adapter | Kape interface | Responsibility |
|---|---|---|
| `PIAPacketTunnelProvider` | `NEPacketTunnelProvider` | Extension entry point; wires up the SDK session/connection controllers. |
| `PIAEndpointRepository` | `VpnConfigurationGenerator` | Resolves endpoints/settings from shared state; autonomously fetches and caches the server list, and ranks candidates by latency (see below). |
| `PIAWireguardAuthenticator` | `PacketTunnelWireguardAuthenticator` | WireGuard key exchange + TLS pinning to the bundled PIA root CA. |
| `PIATunnelLogger` | `PacketTunnelLogger` | Bridges SDK logging to `os.Logger`. |

**One extension, shared source, per-platform target.** A single new Network Extension source
folder, **PlatformSDK-Tunnel** (iOS + tvOS), replaces the per-protocol extensions when active.
The same sources are built by two platform targets — `PlatformSDK-Tunnel-iOS` and
`PlatformSDK-Tunnel-tvOS`, producing `PlatformSDK-Tunnel-iOS.appex` and
`PlatformSDK-Tunnel-tvOS.appex`. The app-side profile
`KapePlatformSDKTunnelProfile: NetworkExtensionProfile` (in `PIALibrary`) configures it.

**App ↔ extension IPC — bidirectional shared state plus a provider message.** State flows in
both directions through `PIATunnelSharedState` (a namespace whose payload is a nested `State`),
persisted as `pia_platformsdk_state.json` in the shared app group (on tvOS under
`Library/Caches`). Every write posts a Darwin notification so the other side observes the change
rather than polling.

- **App → extension (connection inputs).** The extension reads its parameters — selected
server/location, DIP server, protocol, custom DNS, MTU, OpenVPN/WireGuard settings, token, and
app-measured latencies — from shared state. The server list is not solely app-supplied: when
the cached list is stale or absent the extension autonomously fetches a fresh one
(`Client.downloadServerList()`) and caches it back with a TTL, so on-demand reconnects work
with no app running (the `servers` cache is therefore written by both sides).
- **Extension → app (write-back).** Once connected the extension writes back `activeConnection`
(the resolved protocol / region / transport) and a live `tunnelStatus`. The app reads these to
show the actually-resolved endpoint and to drive the *Connecting* UI — `tunnelStatus` is folded
into `VPNStatus.resolve(system:tunnel:)` so mid-session reconnects and in-place region switches
surface as *Connecting* even while `NEVPNStatus` stays `.connected`.
- **Provider message (`switchLocation`).** To change region on a live tunnel without tearing
down the extension process, the app writes the new target to shared state and sends a
`PIAPacketTunnelRequest.switchLocation` message via `sendProviderMessage()`; the extension
re-resolves its endpoints from shared state in place. This replaced an earlier client-side
server-switch marker.

**Three protocol modes, automatic by default.** Protocol selection is mapped through
`KapePlatformSDKVPNType`, which centralises the persisted identifiers — `"PIA"` (OpenVPN),
`"PIAWG"` (WireGuard), and `"PIAAutomatic"` (automatic: a weighted protocol *pecking order* —
WireGuard, then OpenVPN-UDP, then OpenVPN-TCP, each tried against a fixed number of
fastest-first distinct endpoints, with transport, port, and OpenVPN crypto (AES-128-GCM /
SHA256) dictated by the order rather than the user's saved OpenVPN settings) — so
PlatformSDK code never references the legacy `PIATunnelProfile` / `PIAWGTunnelProfile` / TunnelKit
types directly. The enum also carries a fourth, non-connectable `"IKEv2"` case: it exists only to
recognise the value left by pre-PlatformSDK installs so those users can be migrated. **Automatic
is the default** when the flag is on (set in `Bootstrapper` on iOS and `BootstraperFactory` on
tvOS, which also migrate users on an unsupported persisted protocol — e.g. legacy IKEv2 — to
automatic).

**Server resolution.** For a concrete region the extension connects to that server; for a
Dedicated IP it uses the per-user DIP server carried in full through shared state (it is absent
from the public list). For the Automatic region (no selected location) the extension fans out
across every online non-DIP server, fastest first, ordered by the app-measured latencies mirrored
into shared state by `ServersPinger`. When the protocol is also Automatic, endpoint selection
within that fan-out is **protocol-major**: each pecking-order step draws up to its attempt count
of distinct endpoints fastest-first (across the eligible servers) before falling through to the
next protocol, rather than emitting a full per-server WireGuard+OpenVPN batch.

**Reconnection is owned by the SDK.** The engine handles transient network loss and endpoint
cycling internally. When the feature flag is on, `VPNDaemon` suppresses its own reconnect,
fallback-timer, and disconnect-error handling so the app does not fight the SDK's recovery.

**Gated rollout.** The integration is gated behind the CSI-controlled `usePlatformSDKVPN`
feature flag (`ios_platform_sdk_vpn`). On first launch under the flag, the legacy
IKEv2/OpenVPN/WireGuard profiles are removed (`cleanupLegacyVPNProfilesIfNeeded`).

> **Current state:** the flag is temporarily hard-forced `true` in `FeatureFlagHolder` on both
> iOS and tvOS (a `// TODO: [PlatformSDK]` override), so the CSI-driven gating is bypassed while
> the engine is under active development. Removing that override restores CSI control.

## Consequences

- **One maintained tunnel engine** shared across Kape brands; OpenVPN + WireGuard (and, later,
Lightway) run behind a single extension instead of three, with reconnection and path
monitoring handled by the core.
- **Build/CI now depend on a checksum-verified network pull.** Because `PIALibrary` (consumed by
nearly every target) depends on the vendored SDK, no target resolves SPM until
`pull-kape-platform-sdk.sh` has run. Clean checkouts and CI require `CLOUDSMITH_TOKEN`; any new
build entry point must run the pull first or it fails with an opaque SPM error. The pull is
fail-closed on the committed SHA-256 (`scripts/kape-platform-sdk.checksum`), so bumping the
pinned version means updating both the version and checksum pins together.
- **tvOS gains OpenVPN, WireGuard, and Automatic protocol selection** through the same engine
(`ProtocolSelectionView` / `ProtocolSelectionUseCase`).
- **New layering rule:** PlatformSDK code must not reference the legacy tunnel-profile or
TunnelKit types (they are slated for removal); the `KapePlatformSDKVPNType` enum is the seam.
- **Migration / security trade-offs to track.** VPN credentials now flow through the shared-state
file rather than only the Keychain (the OpenVPN password is a protection downgrade vs. the
legacy Keychain-`passwordReference` model), and legacy-profile cleanup is a one-time migration.
- **Forward path:** once the engine is stable behind the flag, the legacy IKEv2/OpenVPN/WireGuard
extensions and the `mobile-ios-openvpn` / `mobile-ios-wireguard` dependencies can be retired.
4 changes: 4 additions & 0 deletions Entitlements/PIA VPN-tvOS.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
<array>
<string>packet-tunnel-provider</string>
</array>
<key>com.apple.developer.networking.vpn.api</key>
<array>
<string>allow-vpn</string>
</array>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.privateinternetaccess</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ public final class VpnConnectionUseCase: VpnConnectionUseCaseType {
connectionIntent.send(.connect)

return try await withCheckedThrowingContinuation { continuation in
vpnProvider.connect { error in
// `changeServer` connects when disconnected and switches in place when already connected
// (on the PlatformSDK tunnel tvOS runs), so the same call covers first connect and region
// change without this use case needing to know the tunnel stack.
vpnProvider.changeServer { error in
if let error = error {
log.error("VPN connect failed: \(error.localizedDescription)")
self.connectionIntent.send(.none)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public struct DebugMenuView: View {
private var vpnSection: some View {
DebugSection("VPN") {
DebugInfoRow(label: "Status", value: Client.daemons.vpnStatus.rawValue)
DebugInfoRow(label: "Protocol", value: Client.preferences.vpnType)
DebugInfoRow(label: "Protocol", value: vpnProtocolDisplayName)
DebugInfoRow(label: "Local IP", value: Client.daemons.publicIP ?? "---")
DebugInfoRow(label: "VPN IP", value: Client.daemons.vpnIP ?? "---")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,24 @@ extension DebugMenuView {
PIALogHandler.logStorage.getAllLogs(includeDebug: true)
}

var vpnProtocolDisplayName: String {
switch Client.preferences.vpnType {
case "PIAWG": return "WireGuard"
case "PIA": return "OpenVPN"
case "IPSec", "IKEv2": return "IKEv2"
default: return Client.preferences.vpnType
}
}

func buildExportContent() -> String {
var lines: [String] = []
lines.append("=== App Info ===")
lines.append("Version: \(appVersion)")
lines.append("Environment: \(environment)")
lines.append("")
lines.append("=== VPN ===")
lines.append("Protocol: \(vpnProtocolDisplayName)")
lines.append("")
lines.append("=== Account ===")
lines.append("Username: \(username)")
lines.append("Plan: \(plan)")
Expand Down
8 changes: 4 additions & 4 deletions LocalPackages/PIALibrary/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ let package = Package(
.package(path: "../PIAAccount"),
.package(path: "../PIARegions"),
.package(url: "git@github.com:pia-foss/mobile-ios-networking.git", exact: "1.3.2"),
.package(url: "git@github.com:pia-foss/mobile-ios-openvpn.git", exact: "2.2.6"),
.package(path: "../KapePlatformSDK/TunnelKitPackage"),
.package(url: "git@github.com:pia-foss/mobile-ios-wireguard.git", exact: "1.0.6"),
.package(url: "https://github.com/apple/swift-algorithms", exact: "1.2.1"),
.package(url: "https://github.com/apple/swift-log", exact: "1.13.1")
Expand All @@ -44,17 +44,17 @@ let package = Package(
),
.product(
name: "TunnelKit",
package: "mobile-ios-openvpn",
package: "TunnelKitPackage",
condition: .when(platforms: [.iOS, .macCatalyst])
),
.product(
name: "TunnelKitOpenVPN",
package: "mobile-ios-openvpn",
package: "TunnelKitPackage",
condition: .when(platforms: [.iOS, .macCatalyst])
),
.product(
name: "TunnelKitOpenVPNAppExtension",
package: "mobile-ios-openvpn",
package: "TunnelKitPackage",
condition: .when(platforms: [.iOS, .macCatalyst])
),
.product(name: "NWHttpConnection", package: "mobile-ios-networking")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ public final class DefaultAccountProvider: AccountProvider, ConfigurationAccess,
Task { @MainActor in
try? await webServices.logout()
cleanDatabase()
PIATunnelSharedState.delete()
Macros.postNotification(.PIAAccountDidLogout)
DispatchQueue.main.async { callback?(nil) }
}
Expand Down
Loading
Loading