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:
Jamil
2025-10-17 08:12:29 -07:00
committed by GitHub
parent 928d8a2512
commit 73576922ff
11 changed files with 127 additions and 94 deletions

View File

@@ -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>";

View File

@@ -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 }

View File

@@ -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)

View File

@@ -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,
]
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)

View File

@@ -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() {

View File

@@ -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?) {

View File

@@ -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)
}
}
}

View File

@@ -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"

View File

@@ -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.