fix(apple): don't call setConfiguration when not connected (#10747)

Skip `setConfiguration()` IPC call when not in connected state; this was
observed as the root cause of the utun interface increments which we've
seen
recently.

Note: `utun` increments can still happen during other IPC calls when not
signed in,
notably during log export when signed out of Firezone. This is not a
major issue though,
as other IPC calls happen only as a result of user interaction between
network extension sleeps.
To fully get rid of the problem, we should address #10754.

To ensure we still are able to pass on configuration before sign in, we
are now
passing configuration directly in the startTunnel() options dictionary.

Fixes #10603
This commit is contained in:
Mariusz Klochowicz
2025-10-29 10:21:50 +10:30
committed by GitHub
parent 04f4415344
commit ac6b3922be
4 changed files with 47 additions and 12 deletions

View File

@@ -50,14 +50,24 @@ class IPCClient {
let decoder = PropertyListDecoder()
// Auto-connect
func start() throws {
try session().startTunnel(options: nil)
@MainActor
func start(configuration: Configuration) throws {
let tunnelConfiguration = configuration.toTunnelConfiguration()
let configData = try encoder.encode(tunnelConfiguration)
let options: [String: NSObject] = [
"configuration": configData as NSObject
]
try session().startTunnel(options: options)
}
// Sign in
func start(token: String) throws {
@MainActor
func start(token: String, configuration: Configuration) throws {
let tunnelConfiguration = configuration.toTunnelConfiguration()
let configData = try encoder.encode(tunnelConfiguration)
let options: [String: NSObject] = [
"token": token as NSObject
"token": token as NSObject,
"configuration": configData as NSObject,
]
try session().startTunnel(options: options)
@@ -76,8 +86,14 @@ class IPCClient {
// 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 wake the extension.
func dryStartStopCycle() throws {
let options: [String: NSObject] = ["dryRun": true as NSObject]
@MainActor
func dryStartStopCycle(configuration: Configuration) throws {
let tunnelConfiguration = configuration.toTunnelConfiguration()
let configData = try encoder.encode(tunnelConfiguration)
let options: [String: NSObject] = [
"dryRun": true as NSObject,
"configuration": configData as NSObject,
]
try session().startTunnel(options: options)
}
#endif
@@ -87,6 +103,10 @@ class IPCClient {
let tunnelConfiguration = configuration.toTunnelConfiguration()
let message = ProviderMessage.setConfiguration(tunnelConfiguration)
if sessionStatus() != .connected {
Log.trace("Not setting configuration whilst not connected")
return
}
try await sendMessageWithoutResponse(message)
}

View File

@@ -193,7 +193,7 @@ public final class Store: ObservableObject {
private func maybeAutoConnect() async throws {
if configuration.connectOnStart {
try await manager().enable()
try ipcClient().start()
try ipcClient().start(configuration: configuration)
}
}
func installVPNConfiguration() async throws {
@@ -232,7 +232,7 @@ public final class Store: ObservableObject {
if vpnStatus == .connected || vpnStatus == .connecting || vpnStatus == .reasserting {
try ipcClient().stop()
} else {
try ipcClient().dryStartStopCycle()
try ipcClient().dryStartStopCycle(configuration: configuration)
}
#else
try ipcClient().stop()
@@ -251,14 +251,13 @@ public final class Store: ObservableObject {
Telemetry.accountSlug = accountSlug
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)
// Bring the tunnel up and send it a token and configuration to start
try ipcClient().start(token: authResponse.token, configuration: configuration)
}
func signOut() async throws {

View File

@@ -68,6 +68,20 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "unknown"
Log.info("Starting tunnel - Version: \(version), Build: \(build)")
// Try to load configuration from options first (passed from client at startup)
if let configData = options?["configuration"] as? Data {
do {
let decoder = PropertyListDecoder()
let configFromOptions = try decoder.decode(TunnelConfiguration.self, from: configData)
Log.info("Loaded configuration from startTunnel options")
// Save it for future fallback (e.g., system-initiated restarts)
configFromOptions.save()
self.tunnelConfiguration = configFromOptions
} catch {
Log.error(error)
}
}
// If the tunnel starts up before the GUI after an upgrade crossing the 1.4.15 version boundary,
// the old system settings-based config will still be present and the new configuration will be empty.
// So handle that edge case gracefully.

View File

@@ -48,7 +48,9 @@ private func forwardEvents(from session: Session, to eventSender: Sender<Event>)
}
/// Forwards commands from the command receiver to the session.
private func forwardCommands(from commandReceiver: Receiver<SessionCommand>, to session: Session) async {
private func forwardCommands(from commandReceiver: Receiver<SessionCommand>, to session: Session)
async
{
for await command in commandReceiver.stream {
if Task.isCancelled {
Log.log("Command forwarding cancelled")