From 994de0fe2a688677ef2afbc374e61e3a07f2989f Mon Sep 17 00:00:00 2001 From: Mariusz Klochowicz Date: Wed, 26 Nov 2025 14:11:17 +1030 Subject: [PATCH] refactor(swift): replace @unchecked Sendable (#10970) VPNConfigurationManager now uses `@MainActor` isolation instead of `@unchecked Sendable`. This aligns with Apple's documented behaviour where NEVPNManager callbacks arrive on the main thread. - Made `VPNConfigurationManager` final and `@MainActor` - Added `@MainActor` to `LogExporter.export(to:session:)` on macOS - Marked `legacyConfiguration()` as `nonisolated` (pure function, called from network extension) - Removed redundant `@MainActor` from `maybeMigrateConfiguration()` --- .../Sources/FirezoneKit/Helpers/LogExporter.swift | 1 + .../Managers/VPNConfigurationManager.swift | 14 ++++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/LogExporter.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/LogExporter.swift index c0a2c78f8..e0f495176 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/LogExporter.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/LogExporter.swift @@ -27,6 +27,7 @@ import System case invalidFileHandle } + @MainActor static func export( to archiveURL: URL, session: NETunnelProviderSession diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Managers/VPNConfigurationManager.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Managers/VPNConfigurationManager.swift index a83052c07..6415b3b35 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Managers/VPNConfigurationManager.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Managers/VPNConfigurationManager.swift @@ -21,10 +21,10 @@ enum VPNConfigurationManagerError: Error { } } -/// Thread-safe: Immutable wrapper around NETunnelProviderManager. -/// Only contains a 'let' property. NETunnelProviderManager handles its own synchronisation. -public class VPNConfigurationManager: @unchecked Sendable { - // Persists our tunnel settings +// NEVPNManager callbacks are documented to arrive on main thread; +// we isolate to @MainActor to align with this design. +@MainActor +public final class VPNConfigurationManager { let manager: NETunnelProviderManager public static let bundleIdentifier: String = "\(Bundle.main.bundleIdentifier!).network-extension" @@ -51,7 +51,10 @@ public class VPNConfigurationManager: @unchecked Sendable { self.manager = manager } - public static func legacyConfiguration(protocolConfiguration: NETunnelProviderProtocol?) + // Pure function - doesn't access actor-isolated state. + nonisolated public static func legacyConfiguration( + protocolConfiguration: NETunnelProviderProtocol? + ) -> [String: String]? { guard let protocolConfiguration = protocolConfiguration, @@ -89,7 +92,6 @@ public class VPNConfigurationManager: @unchecked Sendable { // Firezone 1.4.14 and below stored some app configuration in the VPN provider configuration fields. This has since // been moved to a dedicated UserDefaults-backed persistent store. - @MainActor func maybeMigrateConfiguration() async throws { guard let legacyConfiguration = Self.legacyConfiguration(