diff --git a/swift/apple/Firezone.xcodeproj/project.pbxproj b/swift/apple/Firezone.xcodeproj/project.pbxproj index 16f7bf65c..baabd7bd2 100644 --- a/swift/apple/Firezone.xcodeproj/project.pbxproj +++ b/swift/apple/Firezone.xcodeproj/project.pbxproj @@ -10,7 +10,8 @@ 05CF1CF1290B1CEE00CF4755 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05D3BB1628FDBD8A00BC3727 /* NetworkExtension.framework */; }; 05CF1D17290B1FE700CF4755 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05833DFA28F73B070008FAB0 /* PacketTunnelProvider.swift */; }; 05D3BB2128FDE9C000BC3727 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05D3BB1628FDBD8A00BC3727 /* NetworkExtension.framework */; }; - + 146C8E809FF744D18C053A79 /* Channel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C09DC14A4A04715A37BC04C /* Channel.swift */; }; + 6571861AB9324D6A9395BDB3 /* SessionEventLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14DC5E933BD4F63A844A8A6 /* SessionEventLoop.swift */; }; 6F0EDF1F2E79A13800D6D632 /* Adapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 177FB893F19457A113042247 /* Adapter.swift */; }; 6F0EDF212E79A15700D6D632 /* connlib.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2847ACC9D73EA6F356F2DEE1 /* connlib.swift */; }; 6FE93AFB2A738D7E002D278A /* NetworkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE93AFA2A738D7E002D278A /* NetworkSettings.swift */; }; @@ -25,10 +26,6 @@ 8D5048002CE6AA60009802E9 /* SystemConfigurationResolvers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D6939312BA2521A00AF4396 /* SystemConfigurationResolvers.swift */; }; 8D5048012CE6AA60009802E9 /* NetworkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE93AFA2A738D7E002D278A /* NetworkSettings.swift */; }; 8D69392C2BA24FE600AF4396 /* BindResolvers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D69392B2BA24FE600AF4396 /* BindResolvers.swift */; }; - 146C8E809FF744D18C053A79 /* Channel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C09DC14A4A04715A37BC04C /* Channel.swift */; }; - C1892134991C462C8E137DE3 /* Channel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C09DC14A4A04715A37BC04C /* Channel.swift */; }; - C4B08E1145E04823BC25E00D /* SessionEventLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14DC5E933BD4F63A844A8A6 /* SessionEventLoop.swift */; }; - 6571861AB9324D6A9395BDB3 /* SessionEventLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14DC5E933BD4F63A844A8A6 /* SessionEventLoop.swift */; }; 8DC08BD72B297DB400675F46 /* FirezoneNetworkExtension-Bridging-Header.h in Sources */ = {isa = PBXBuildFile; fileRef = 6FE455112A5D13A2006549B1 /* FirezoneNetworkExtension-Bridging-Header.h */; }; 8DC1699D2CFF77D1006801B5 /* dev.firezone.firezone.network-extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 05CF1CF0290B1CEE00CF4755 /* dev.firezone.firezone.network-extension.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 8DC169A02CFF77D1006801B5 /* dev.firezone.firezone.network-extension.systemextension in Embed System Extensions */ = {isa = PBXBuildFile; fileRef = 8D5047E32CE6A8F4009802E9 /* dev.firezone.firezone.network-extension.systemextension */; platformFilters = (macos, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -41,7 +38,8 @@ 8DCC022A28D512AE007E12D2 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8DCC022928D512AE007E12D2 /* Preview Assets.xcassets */; }; 8DE452A82CE6C194004CEDF9 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D5047E82CE6A8F4009802E9 /* main.swift */; }; 8DFDEAC72D2CEBA500615095 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 8DA12C322BB7DA04007D91EB /* PrivacyInfo.xcprivacy */; }; - + C1892134991C462C8E137DE3 /* Channel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C09DC14A4A04715A37BC04C /* Channel.swift */; }; + C4B08E1145E04823BC25E00D /* SessionEventLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14DC5E933BD4F63A844A8A6 /* SessionEventLoop.swift */; }; CCC04BEF428B758D9BB5F842 /* connlib.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2847ACC9D73EA6F356F2DEE1 /* connlib.swift */; }; /* End PBXBuildFile section */ @@ -88,13 +86,13 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 05833DFA28F73B070008FAB0 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = ""; }; 05CF1CF0290B1CEE00CF4755 /* dev.firezone.firezone.network-extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "dev.firezone.firezone.network-extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 05CF1CF6290B1CEE00CF4755 /* FirezoneNetworkExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FirezoneNetworkExtension.entitlements; sourceTree = ""; }; 05D3BB1628FDBD8A00BC3727 /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; }; 177FB893F19457A113042247 /* Adapter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Adapter.swift; sourceTree = ""; }; 2847ACC9D73EA6F356F2DEE1 /* connlib.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = connlib.swift; path = FirezoneNetworkExtension/Connlib/Generated/connlib.swift; sourceTree = ""; }; + 6C09DC14A4A04715A37BC04C /* Channel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Channel.swift; sourceTree = ""; }; 6FB20C422E7049A300E41294 /* ConnlibFFI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ConnlibFFI.xcframework; path = Frameworks/ConnlibFFI.xcframework; sourceTree = ""; }; 6FE455112A5D13A2006549B1 /* FirezoneNetworkExtension-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FirezoneNetworkExtension-Bridging-Header.h"; sourceTree = ""; }; 6FE93AFA2A738D7E002D278A /* NetworkSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSettings.swift; sourceTree = ""; }; @@ -105,8 +103,6 @@ 8D5047E32CE6A8F4009802E9 /* dev.firezone.firezone.network-extension.systemextension */ = {isa = PBXFileReference; explicitFileType = "wrapper.system-extension"; includeInIndex = 0; path = "dev.firezone.firezone.network-extension.systemextension"; sourceTree = BUILT_PRODUCTS_DIR; }; 8D5047E82CE6A8F4009802E9 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 8D69392B2BA24FE600AF4396 /* BindResolvers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BindResolvers.swift; sourceTree = ""; }; - 6C09DC14A4A04715A37BC04C /* Channel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Channel.swift; sourceTree = ""; }; - E14DC5E933BD4F63A844A8A6 /* SessionEventLoop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionEventLoop.swift; sourceTree = ""; }; 8D6939312BA2521A00AF4396 /* SystemConfigurationResolvers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemConfigurationResolvers.swift; sourceTree = ""; }; 8DA12C322BB7DA04007D91EB /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 8DC08BCC2B296C5900675F46 /* libresolv.9.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.9.tbd; path = usr/lib/libresolv.9.tbd; sourceTree = SDKROOT; }; @@ -121,6 +117,7 @@ 8DDD0E8B2ADC6657001FA7E9 /* config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = config.xcconfig; path = xcconfig/config.xcconfig; sourceTree = ""; }; 8DE1077A2D2313EB00DB5A45 /* Info.iOS.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.iOS.plist; sourceTree = ""; }; 8DE1077B2D2313EB00DB5A45 /* Info.macOS.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.macOS.plist; sourceTree = ""; }; + E14DC5E933BD4F63A844A8A6 /* SessionEventLoop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionEventLoop.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -168,6 +165,7 @@ E14DC5E933BD4F63A844A8A6 /* SessionEventLoop.swift */, 05CF1CF6290B1CEE00CF4755 /* FirezoneNetworkExtension.entitlements */, 8D5047E82CE6A8F4009802E9 /* main.swift */, + 177FB893F19457A113042247 /* Adapter.swift */, 05833DFA28F73B070008FAB0 /* PacketTunnelProvider.swift */, 6FE93AFA2A738D7E002D278A /* NetworkSettings.swift */, 8D41B9A42D15DD6800D16065 /* TunnelLogArchive.swift */, @@ -197,7 +195,6 @@ isa = PBXGroup; children = ( 64A772F0917639D7745EA995 /* Connlib */, - 177FB893F19457A113042247 /* Adapter.swift */, ); path = FirezoneNetworkExtension; sourceTree = ""; diff --git a/swift/apple/Firezone/Application/FirezoneApp.swift b/swift/apple/Firezone/Application/FirezoneApp.swift index 9760d3897..c67e563ae 100644 --- a/swift/apple/Firezone/Application/FirezoneApp.swift +++ b/swift/apple/Firezone/Application/FirezoneApp.swift @@ -76,6 +76,9 @@ struct FirezoneApp: App { func applicationWillFinishLaunching(_ notification: Notification) { // Enforce single instance BEFORE the app fully launches enforceSingleInstance() + + // Prevent sudden termination for menu bar apps to allow cleanup + ProcessInfo.processInfo.disableSuddenTermination() } func applicationDidFinishLaunching(_: Notification) { @@ -91,6 +94,23 @@ struct FirezoneApp: App { maybeShowOutdatedAlert() } + func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { + guard let store else { + return .terminateNow + } + + Task { + do { try await store.stop() } catch { Log.error(error) } + await MainActor.run { NSApp.reply(toApplicationShouldTerminate: true) } + } + + return .terminateLater + } + + func applicationWillTerminate(_ notification: Notification) { + Log.log("\(#function) - app is about to quit") + } + private func enforceSingleInstance() { // Get the actual bundle identifier from the running app guard let bundleId = Bundle.main.bundleIdentifier else { return } diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/IPCClient.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/IPCClient.swift index 6ea2dea8b..25c2d172a 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/IPCClient.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Helpers/IPCClient.swift @@ -75,8 +75,8 @@ class IPCClient { #if os(macOS) // On macOS, IPC calls to the system extension won't work after it's been upgraded, until the startTunnel call. // Since we rely on IPC for the GUI to function, we need to send a dummy `startTunnel` that doesn't actually - // start the tunnel, but causes the system to start the extension. - func startSystemExtension() throws { + // start the tunnel, but causes the system to wake the extension. + func dryStartStopCycle() throws { let options: [String: NSObject] = ["dryRun": true as NSObject] try session().startTunnel(options: options) } @@ -198,30 +198,6 @@ class IPCClient { loop() } - func consumeStopReason() async throws -> NEProviderStopReason? { - return try await withCheckedThrowingContinuation { continuation in - do { - try session().sendProviderMessage( - encoder.encode(ProviderMessage.consumeStopReason) - ) { data in - - guard let data = data, - let reason = String(data: data, encoding: .utf8), - let rawValue = Int(reason) - else { - continuation.resume(returning: nil) - - return - } - - continuation.resume(returning: NEProviderStopReason(rawValue: rawValue)) - } - } catch { - continuation.resume(throwing: error) - } - } - } - // Subscribe to system notifications about our VPN status changing // and let our handler know about them. func subscribeToVPNStatusUpdates(handler: @escaping @MainActor (NEVPNStatus) async throws -> Void) diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/ConnlibError.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/ConnlibError.swift new file mode 100644 index 000000000..291f11ff2 --- /dev/null +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/ConnlibError.swift @@ -0,0 +1,34 @@ +// +// ConnlibError.swift +// © 2025 Firezone, Inc. +// LICENSE: Apache-2.0 +// + +import Foundation + +public enum ConnlibError: Swift.Error { + case sessionExpired(String, id: String = UUID().uuidString) +} + +extension ConnlibError: CustomNSError { + public static var errorDomain: String { + return "FirezoneKit.ConnlibError" + } + + public var errorCode: Int { + switch self { + case .sessionExpired: + return 0 + } + } + + public var errorUserInfo: [String: Any] { + switch self { + case .sessionExpired(let reason, let id): + return [ + "reason": reason, + "id": id, + ] + } + } +} diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/ProviderMessage.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/ProviderMessage.swift index 1ba23795a..041d4025e 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/ProviderMessage.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/ProviderMessage.swift @@ -14,7 +14,6 @@ public enum ProviderMessage: Codable { case clearLogs case getLogFolderSize case exportLogs - case consumeStopReason enum CodingKeys: String, CodingKey { case type @@ -28,7 +27,6 @@ public enum ProviderMessage: Codable { case clearLogs case getLogFolderSize case exportLogs - case consumeStopReason } public init(from decoder: Decoder) throws { @@ -49,8 +47,6 @@ public enum ProviderMessage: Codable { self = .getLogFolderSize case .exportLogs: self = .exportLogs - case .consumeStopReason: - self = .consumeStopReason } } @@ -71,8 +67,6 @@ public enum ProviderMessage: Codable { try container.encode(MessageType.getLogFolderSize, forKey: .type) case .exportLogs: try container.encode(MessageType.exportLogs, forKey: .type) - case .consumeStopReason: - try container.encode(MessageType.consumeStopReason, forKey: .type) } } } diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/SessionNotification.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/SessionNotification.swift index 5d11af9e1..94c7205ad 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/SessionNotification.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/SessionNotification.swift @@ -98,10 +98,14 @@ public class SessionNotification: NSObject { // In macOS, use a Cocoa alert. // This gets called from the app side. @MainActor - func showSignedOutAlertmacOS() async { + func showSignedOutAlertmacOS(_ message: String?) async { let alert = NSAlert() alert.messageText = "Your Firezone session has ended" - alert.informativeText = "Please sign in again to reconnect" + alert.informativeText = """ + Please sign in again to reconnect. + + \(message ?? "") + """ alert.addButton(withTitle: "Sign In") alert.addButton(withTitle: "Cancel") NSApp.activate(ignoringOtherApps: true) diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/Store.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/Store.swift index 44318f4bb..7259d9cf5 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/Store.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/Store.swift @@ -42,11 +42,15 @@ public final class Store: ObservableObject { private var vpnConfigurationManager: VPNConfigurationManager? private var cancellables: Set = [] + // Track which session expired alerts have been shown to prevent duplicates + private var shownAlertIds: Set + public init(configuration: Configuration? = nil) { self.configuration = configuration ?? Configuration.shared // Load GUI-only cached state self.actorName = UserDefaults.standard.string(forKey: "actorName") ?? "Unknown user" + self.shownAlertIds = Set(UserDefaults.standard.stringArray(forKey: "shownAlertIds") ?? []) self.sessionNotification.signInHandler = { Task { @@ -126,11 +130,25 @@ public final class Store: ObservableObject { #if os(macOS) // On macOS we must show notifications from the UI process. On iOS, we've already initiated the notification // from the tunnel process, because the UI process is not guaranteed to be alive. - if vpnStatus == .disconnected { + + // fetchLastDisconnectError is only available on macOS 13+ + if #available(macOS 13, *), vpnStatus == .disconnected { do { - let reason = try await ipcClient().consumeStopReason() - if reason == .authenticationCanceled { - await self.sessionNotification.showSignedOutAlertmacOS() + try manager().session()?.fetchLastDisconnectError { error in + if let nsError = error as NSError?, + nsError.domain == ConnlibError.errorDomain, + nsError.code == 0, // sessionExpired error code + let reason = nsError.userInfo["reason"] as? String, + let id = nsError.userInfo["id"] as? String + { + // Only show the alert if we haven't shown this specific error before + Task { @MainActor in + if !self.shownAlertIds.contains(id) { + await self.sessionNotification.showSignedOutAlertmacOS(reason) + self.markAlertAsShown(id) + } + } + } } } catch { Log.error(error) @@ -207,8 +225,18 @@ public final class Store: ObservableObject { self.decision = try await sessionNotification.askUserForNotificationPermissions() } - func stop() throws { - try ipcClient().stop() + public func stop() async throws { + #if os(macOS) + // On macOS, the system removes the utun interface on stop ONLY if the VPN is in a connected state. + // So we need to do a dry run start-then-stop if we're not connected, to ensure the interface is removed. + if vpnStatus == .connected || vpnStatus == .connecting || vpnStatus == .reasserting { + try ipcClient().stop() + } else { + try ipcClient().dryStartStopCycle() + } + #else + try ipcClient().stop() + #endif } func signIn(authResponse: AuthResponse) async throws { @@ -225,6 +253,10 @@ public final class Store: ObservableObject { try await manager().enable() try await ipcClient().setConfiguration(configuration) + // Clear shown alerts when starting a new session so user can see new errors + shownAlertIds.removeAll() + UserDefaults.standard.removeObject(forKey: "shownAlertIds") + // Bring the tunnel up and send it a token to start try ipcClient().start(token: authResponse.token) } @@ -239,6 +271,11 @@ public final class Store: ObservableObject { // MARK: Private functions + private func markAlertAsShown(_ id: String) { + shownAlertIds.insert(id) + UserDefaults.standard.set(Array(shownAlertIds), forKey: "shownAlertIds") + } + // Network Extensions don't have a 2-way binding up to the GUI process, // so we need to periodically ask the tunnel process for them. private func beginUpdatingResources() { diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift index e205b8cc9..960b5546b 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift @@ -802,10 +802,7 @@ import SwiftUI } @objc func quitButtonTapped() { - Task { - do { try store.stop() } catch { Log.error(error) } - NSApp.terminate(self) - } + NSApp.terminate(self) } @objc func resourceValueTapped(_ sender: AnyObject?) { diff --git a/swift/apple/FirezoneNetworkExtension/Adapter.swift b/swift/apple/FirezoneNetworkExtension/Adapter.swift index 83ea2626f..62ee72ae3 100644 --- a/swift/apple/FirezoneNetworkExtension/Adapter.swift +++ b/swift/apple/FirezoneNetworkExtension/Adapter.swift @@ -29,7 +29,7 @@ enum AdapterError: Error { case .connlibConnectError(let error): return "connlib failed to start: \(error)" case .setDnsError(let error): - return "failed to set new DNS serversn: \(error)" + return "failed to set new DNS servers: \(error)" } } } @@ -396,28 +396,18 @@ class Adapter: @unchecked Sendable { return } - // If auth expired/is invalid, delete stored token and save the reason why so the GUI can act upon it. if error.isAuthenticationError() { - // Delete stored token and save the reason for the GUI - do { - try Token.delete() - let reason: NEProviderStopReason = .authenticationCanceled - try String(reason.rawValue).write( - to: SharedAccess.providerStopReasonURL, atomically: true, encoding: .utf8) - } catch { - Log.error(error) - } - #if os(iOS) // iOS notifications should be shown from the tunnel process SessionNotification.showSignedOutNotificationiOS() #endif - } else { - Log.warning("Disconnected with error: \(errorMessage)") - } - // Handle disconnection - provider.cancelTunnelWithError(nil) + let error = FirezoneKit.ConnlibError.sessionExpired(errorMessage) + + provider.cancelTunnelWithError(error) + } else { + provider.cancelTunnelWithError(nil) + } } } diff --git a/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift b/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift index 9598aadfc..b3dfd85dc 100644 --- a/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift +++ b/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift @@ -13,6 +13,7 @@ enum PacketTunnelProviderError: Error { case tunnelConfigurationIsInvalid case firezoneIdIsInvalid case tokenNotFoundInKeychain + case dryStartStopCycle } class PacketTunnelProvider: NEPacketTunnelProvider { @@ -55,13 +56,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider { options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void ) { - super.startTunnel(options: options, completionHandler: completionHandler) - // Dummy start to get the extension running on macOS after upgrade if options?["dryRun"] as? Bool == true { Log.info("Dry run startup requested - extension awakened but not starting tunnel") - completionHandler(nil) - return + return completionHandler(PacketTunnelProviderError.dryStartStopCycle) } // Log version on actual tunnel start @@ -149,8 +147,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { // handles both connlib-initiated and user-initiated stops adapter?.stop() - - super.stopTunnel(with: reason, completionHandler: completionHandler) + completionHandler() } // It would be helpful to be able to encapsulate Errors here. To do that @@ -192,9 +189,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider { getLogFolderSize(completionHandler) case .exportLogs: exportLogs(completionHandler!) - - case .consumeStopReason: - consumeStopReason(completionHandler!) } } catch { Log.error(error) @@ -303,20 +297,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider { } } - func consumeStopReason(_ completionHandler: (Data?) -> Void) { - guard let data = try? Data(contentsOf: SharedAccess.providerStopReasonURL) - else { - completionHandler(nil) - - return - } - - try? FileManager.default - .removeItem(at: SharedAccess.providerStopReasonURL) - - completionHandler(data) - } - // Firezone ID migration. Can be removed once most clients migrate past 1.4.15. private func migrateFirezoneId() { let filename = "firezone-id" diff --git a/website/src/components/Changelog/Apple.tsx b/website/src/components/Changelog/Apple.tsx index 6bcfb2445..c7ac9daea 100644 --- a/website/src/components/Changelog/Apple.tsx +++ b/website/src/components/Changelog/Apple.tsx @@ -25,6 +25,10 @@ export default function Apple() { {/* When you cut a release, remove any solved issues from the "known issues" lists over in `client-apps`. This must not be done when the issue's PR merges. */} + + Fixes an issue on macOS where DNS resources might fail to be routed + properly after many (150+) Firezone session restarts. + Fixes an issue where the Internet Resource could be briefly active on startup, despite it being disabled.