mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-28 02:18:50 +00:00
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