KM-17461: decouple VPN permission grant from server list availability - #373
Conversation
After a fresh signup the server list is empty (logout wipes the cache and the re-download is daemon-driven), so install() could not build a profile (noServersAvailable) and the permission screen looped an undismissable "We need this permission" alert. - Add VPNProvider.obtainVPNPermission(): saves the profile with a placeholder server when no target server is available yet, so the one-time OS VPN permission no longer depends on the server list. connect() already re-resolves and re-saves the real server before starting the tunnel. - VPNPermissionViewController: use obtainVPNPermission() and stop auto-retrying from the alert's OK action (the loop mechanism). - PIATunnelProfile: fall back to the server hostname instead of persisting an empty serverAddress when no OpenVPN address resolves. - Add VPNPermissionPlaceholderTests locking the non-empty serverAddress invariant for WireGuard and OpenVPN.
|
Claude finished @kp-nils-crefeld's task in 7m 35s —— View job Code review: KM-17461 — decouple VPN permission grant from server list
OverallThe diagnosis is right and the fix is well-scoped: the OS VPN permission is a one-time grant that has no business depending on a downloaded server list, and I filed 6 inline comments. The ones I'd want addressed before merge: 1. 2. Placeholder hostname is inside the real PIA domain — 3. Test file will not compile for tvOS — 4. On-demand on the placeholder profile — the placeholder configuration still inherits Two things outside the difftvOS has the identical bug and isn't fixed. The first Test coverageThe two added tests lock a real invariant, but they're the least valuable of the available options: WireGuard already fell back to the hostname before this PR, and Memory / threadingNo retain cycles introduced. The Flows to test
Verification I could not runThis runner is Linux with no Xcode/simulator, so I did not build the project, run |
- Placeholder server: use RFC 6761 ".invalid" hostname so it can never resolve nor match the needsMigrationToGEN4() PIA-domain check; expose it as a computed property instead of a shared mutable instance. - Never save a placeholder profile with on-demand rules, so the OS cannot autoconnect to the placeholder via a leftover enabled manager. - VPNPermissionViewController: hop to the main queue and capture self weakly in the permission callback. - Tests: guard for iOS, exercise the real OpenVPN endpointProtocols branch via a production-like configuration, restore lastServerCN.
|
Claude finished @kp-nils-crefeld's task in 3m 52s —— View job Follow-up review: commit
|
| # | Finding | Status |
|---|---|---|
| 1 | Shared mutable static let placeholder |
✅ now a computed static var — fresh instance per access, no call-site churn |
| 1b | Hostname inside the PIA domain | ✅ vpn-permission-placeholder.invalid — RFC 6761, cannot resolve, and no longer matches the address.contains("privateinternetaccess.com") test in needsMigrationToGEN4() |
| 2 | On-demand on the placeholder profile | ✅ isOnDemand: usesServerPlaceholder ? false : accessedPreferences.isPersistentConnection (DefaultVPNProvider.swift:453) |
| 3 | Test file wouldn't compile for tvOS | ✅ #if os(iOS) guard; indentation matches .swift-format's indentConditionalCompilationBlocks: true, imports satisfy OrderedImports |
| 4 | OpenVPN test bypassed the real branch | ✅ now builds a production-like OpenVPNProvider.Configuration |
| 5 | lastServerCN not restored |
✅ saved in setUp, restored in tearDown (it's String?, so the optional restore compiles) |
| 6 | Callback queue + strong self in the VC |
✅ [weak self] + explicit DispatchQueue.main.async |
I traced the OpenVPN test to confirm it now walks the branch it claims: endpointProtocols contains a TCP entry → PIATunnelProfile.swift:299 takes the TCP path → bestAddressForOVPN(tcp: true) returns nil on the empty-address placeholder (Server.swift:292-294) → serverAddress stays "" → the new hostname fallback fires. That's faithful to the production failure now.
One new caveat from the on-demand fix (non-blocking)
RootCoordinator.handleAuthenticationSuccess() presents the permission screen after login as well as signup (RootCoordinator.swift:163-171), unconditionally — there's no check for permission already being granted. So for a returning user who logs back in while the server cache is empty:
doSaveevaluatesvpn.isOnDemandEnabled = isVPNConnected || vpn.isEnabled ? configuration.isOnDemand : false(NetworkExtensionProfile.swift:72). With an already-enabled manager and the placeholder'sisOnDemand: false, this clears their on-demand rules whileClient.preferences.isPersistentConnectionstaystrue→ system state and preference diverge until the nextconnect()/save re-writes it.- The saved profile also shows
vpn-permission-placeholder.invalidin Settings › VPN until the first connect (cosmetic, but worth eyeballing on device).
This isn't a regression — pre-PR that path errored out entirely — and it self-heals on the next connect. If you want it fully closed, the cheapest options are to skip the save when the OS permission already exists, or to preserve the manager's current isOnDemandEnabled on the placeholder path instead of forcing false. Your call whether that's worth it versus the autoconnect hazard the false closes. Fix this →
Minor nit
Package.swift declares TunnelKitOpenVPN for the test target, but the test also does import TunnelKitCore, which isn't declared. It compiles — it resolves transitively, exactly as VPNDaemon.swift and PIATunnelProvider+Profile.swift already do in Sources without declaring it — so this is consistency, not a build break. Declaring it alongside (same .when(platforms:) condition) would be more honest.
Still open by choice — fine to defer, just confirming scope
obtainVPNPermissionremains a barepublic protocolrequirement (VPNProvider.swift:68) with no default implementation — source-breaking for out-of-repo conformers only.vpnClientConfiguration(for:allowServerPlaceholder:)is stillprivate, so the placeholder-vs-nilbranch itself has no direct test (the tests cover the profile-generation invariant instead).- tvOS still has the same bug:
InstallVpnConfigurationProvider.swift:22callsinstall(force: true)and maps any error to.userCanceled. PIAWGTunnelProfile.swift:294still writeslastServerCNunconditionally, so a WireGuard permission-only save clobbers a previous CN with""(pre-existing, low impact).- The first
connect()beforeServersDaemonlands still returnsvpnClientConfigurationUnavailable— same root cause, one screen later. Re-loadingconfiguration.bundledServersJSONafter logout remains the broader fix if you want a follow-up ticket.
Verification I could not run
Linux runner with no Xcode/simulator: no xcodebuild, no bundle exec fastlane iOStests, and SPM dependencies use git@github.com: SSH remotes so I couldn't resolve/build the package either. swift format is present on the runner but running it wasn't permitted in this job, so formatting is eyeballed against .swift-format, not verified. The tvOS-compilation fix (#3) and the new TunnelKitOpenVPN test dependency are the two things most worth confirming with a local build before merge.
Verdict: the review feedback is addressed correctly and the changes hold up on re-reading. Nothing blocking from my side.
• branch KM-17461-vpn-permission-placeholder
Summary
After a fresh signup the server list is empty (logout wipes the cache and the re-download is daemon-driven), so install() could not build a profile (noServersAvailable) and the permission screen looped an undismissable "We need this permission" alert.