mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
fix(apple/macos): clean up utun on quit (#10603)
On macOS, because it uses the System Extension packaging type, the lifecycle of the tunnel provider process is not tied directly to connlib's session start and end, but rather managed by the system. The process is likely running at all times, even when the GUI is not open or signed in. The system will start the provider process upon the first IPC call to it, which allocates a `utun` interface. The tricky part is ensuring this interface gets removed when the GUI app quits. Otherwise, it's likely that upon the next launch of the GUI app, the system will allocate a _new_ utun interface, and the old one will linger until the next system reboot. Here's where things get strange. The system will only remove the `utun` interface when stopping the tunnel under the following conditions: - The provider is currently not in a `disconnected` state (so it needs to be in `reasserting`, `connecting`, or `connected` - The GUI side has called `stopTunnel`, thereby invoking the provider's `stopTunnel` override function, or - The provider side has called `cancelTunnelWithError`, or - The `startTunnel`'s completionHandler is called with an `Error` The problem we had is that we make various IPC calls throughout the lifecycle of the GUI app, for example, to gather logs, set tunnel configuration, and the like. If the GUI app was _not_ in a connected state when the user quit, the `utun` would linger, even though we were issuing a final `stopTunnel` upon quit in all circumstances. To fix the issue, we update the dry run `startTunnel` code path we added previously in two ways: 1. We add a `dryRun` error type to the `startTunnel`'s completionHandler 2. We implement the GUI app `applicationShouldTerminate` handler in order to trigger one final dryRun which briefly moves the provider to a connected state so the system will clean us up when its completionHandler is invoked. Tested under the following conditions: - Launch app in a signed-out state -> quit - Launch app in a signed-out state -> sign in -> quit - Launch app in a signed-out state -> sign in -> sign out -> quit - Launch app in a signed-in state -> quit - Launch app in a signed-in state -> sign out -> quit Notably, if the GUI app is killed with `SIGKILL`, our terminate hook is _not_ called, and the utun lingers. We'll have to accept this edge case for now. Along with the above, the janky `consumeStopReason` mechanism has been removed in favor of NE's `cancelTunnelWithError` to pass the error back to the GUI we can then use to show the signed out alert. Fixes #10580
This commit is contained in:
@@ -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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
2847ACC9D73EA6F356F2DEE1 /* connlib.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = connlib.swift; path = FirezoneNetworkExtension/Connlib/Generated/connlib.swift; sourceTree = "<group>"; };
|
||||
6C09DC14A4A04715A37BC04C /* Channel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Channel.swift; sourceTree = "<group>"; };
|
||||
6FB20C422E7049A300E41294 /* ConnlibFFI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ConnlibFFI.xcframework; path = Frameworks/ConnlibFFI.xcframework; sourceTree = "<group>"; };
|
||||
6FE455112A5D13A2006549B1 /* FirezoneNetworkExtension-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FirezoneNetworkExtension-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
6FE93AFA2A738D7E002D278A /* NetworkSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSettings.swift; sourceTree = "<group>"; };
|
||||
@@ -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 = "<group>"; };
|
||||
8D69392B2BA24FE600AF4396 /* BindResolvers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BindResolvers.swift; sourceTree = "<group>"; };
|
||||
6C09DC14A4A04715A37BC04C /* Channel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Channel.swift; sourceTree = "<group>"; };
|
||||
E14DC5E933BD4F63A844A8A6 /* SessionEventLoop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionEventLoop.swift; sourceTree = "<group>"; };
|
||||
8D6939312BA2521A00AF4396 /* SystemConfigurationResolvers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemConfigurationResolvers.swift; sourceTree = "<group>"; };
|
||||
8DA12C322BB7DA04007D91EB /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
8DE1077A2D2313EB00DB5A45 /* Info.iOS.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.iOS.plist; sourceTree = "<group>"; };
|
||||
8DE1077B2D2313EB00DB5A45 /* Info.macOS.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.macOS.plist; sourceTree = "<group>"; };
|
||||
E14DC5E933BD4F63A844A8A6 /* SessionEventLoop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionEventLoop.swift; sourceTree = "<group>"; };
|
||||
/* 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 = "<group>";
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -42,11 +42,15 @@ public final class Store: ObservableObject {
|
||||
private var vpnConfigurationManager: VPNConfigurationManager?
|
||||
private var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
// Track which session expired alerts have been shown to prevent duplicates
|
||||
private var shownAlertIds: Set<String>
|
||||
|
||||
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() {
|
||||
|
||||
@@ -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?) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -25,6 +25,10 @@ export default function Apple() {
|
||||
<Entries downloadLinks={downloadLinks} title="macOS / iOS">
|
||||
{/* 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. */}
|
||||
<Unreleased>
|
||||
<ChangeItem pull="10603">
|
||||
Fixes an issue on macOS where DNS resources might fail to be routed
|
||||
properly after many (150+) Firezone session restarts.
|
||||
</ChangeItem>
|
||||
<ChangeItem pull="10509">
|
||||
Fixes an issue where the Internet Resource could be briefly active on
|
||||
startup, despite it being disabled.
|
||||
|
||||
Reference in New Issue
Block a user