Prompted by Xcode warning at project startup.
Most of the changes are simple migrations from entitlements files
to build settings, which is the recommended approach, and were done
automatically by Xcode.
new settings:
- REGISTER_APP_GROUPS - Automatically registers app groups with
provisioning
profile (I had to set this manually when setting up, so it's a welcome
change)
- STRING_CATALOG_GENERATE_SYMBOLS - type-safe localization (no
regression, we're not doing any localization currently)
- ENABLE_USER_SCRIPT_SANDBOXING - sandboxing all the build scripts
Note: I had to turn off the recommended `ENABLE_USER_SCRIPT_SANDBOXING`
as it
would interfere with our building of connlib during the build.
Also: make Makefile more ergonomic to use (setup LSP config during first
build)
Enables SWIFT_APPROACHABLE_CONCURRENCY build setting which activates
a few key Swift 6.2 concurrency features, including:
1. NonisolatedNonsendingByDefault - Makes nonisolated async functions
run
on the caller's executor instead of the global executor, providing
more predictable performance and behaviour
2. InferIsolatedConformances - Protocol conformances automatically
inherit global actor isolation, reducing annotation burden
Read more:
https://www.donnywals.com/what-is-approachable-concurrency-in-xcode-26/
Also bumps swift-tools-version from 6.0 to 6.2 in Package.swift to
enable newer Package Manager manifest APIs.
As a result of better type inference, removes 1 redundant @Sendable
annotation in Store.swift:
- vpnStatusChangeHandler: @MainActor closures are implicitly Sendable
This PR upgrades the Swift client from Swift 5 to Swift 6.2, addressing
all
concurrency-related warnings and runtime crashes that come with Swift
6's
strict concurrency checking.
## Swift 6 Concurrency Primer
**`actor`** - A new reference type that provides thread-safe, serialised
access to mutable state. Unlike classes, actors ensure that only one
piece of
code can access their mutable properties at a time. Access to actor
methods/properties requires await and automatically hops to the actor's
isolated executor.
**`@MainActor`** - An attribute that marks code to run on the main
thread.
Essential for UI updates and anything that touches UIKit/AppKit. When a
class/function is marked @MainActor, all its methods and properties
inherit
this isolation.
**`@Sendable`** - A protocol indicating that a type can be safely passed
across concurrency domains (between actors, tasks, etc.). Value types
(structs, enums) with Sendable stored properties are automatically
Sendable.
Reference types (classes) need explicit @unchecked Sendable if they
manage
thread-safety manually.
**`nonisolated`** - Opts out of the containing type's actor isolation.
For
example, a nonisolated method in a @MainActor class can be called from
any
thread without await. Useful for static methods or thread-safe
operations.
**`@concurrent`** - Used on closure parameters in delegate methods.
Indicates
the closure may be called from any thread, preventing the closure from
inheriting the surrounding context's actor isolation. Critical for
callbacks
from system frameworks that call from background threads.
**Data Races** - Swift 6 enforces at compile-time (and optionally at
runtime)
that mutable state cannot be accessed concurrently from multiple
threads. This
eliminates entire classes of bugs that were previously only caught
through
testing or production crashes.
## Swift Language Upgrade
- **Bump Swift 5 → 6.2**: Enabled strict concurrency checking throughout
the
codebase
- **Enable ExistentialAny (SE-0335)**: Adds compile-time safety by
making
protocol type erasure explicit (e.g., any Protocol instead of implicit
Protocol)
- **Runtime safety configuration**: Added environment variables to log
concurrency violations during development instead of crashing, allowing
gradual migration
## Concurrency Fixes
### Actor Isolation
- **TelemetryState actor** (Telemetry.swift:10): Extracted mutable
telemetry
state into a dedicated actor to eliminate data races from concurrent
access
- **SessionNotification @MainActor isolation**
(SessionNotification.swift:25):
Properly isolated the class to MainActor since it manages UI-related
callbacks
- **IPCClient caching** (IPCClient.swift): Fixed actor re-entrance
issues and
resource hash-based optimisation by caching the client instance in Store
### Thread-Safe Callbacks
- **WebAuthSession @concurrent delegate** (WebAuthSession.swift:46): The
authentication callback is invoked from a background thread by
ASWebAuthenticationSession. Marked the wrapper function as @concurrent
to
prevent MainActor inference on the completion handler closure, then
explicitly hopped back to MainActor for the session.start() call. This
fixes EXC_BAD_INSTRUCTION crashes at _dispatch_assert_queue_fail.
- **SessionNotification @concurrent delegate**
(SessionNotification.swift:131): Similarly marked the notification
delegate
method as @concurrent and used Task { @MainActor in } to safely invoke
the
MainActor-isolated signInHandler
### Sendable Conformances
- Added Sendable to Resource, Site, Token, Configuration, and other
model
types that are passed between actors and tasks
- **LogWriter immutability** (Log.swift): Made jsonData immutable to
prevent
capturing mutable variables in @Sendable closures
### Nonisolated Methods
- **Static notification display** (SessionNotification.swift:73): Marked
showSignedOutNotificationiOS() as nonisolated since it's called from the
Network Extension (different process) and only uses thread-safe APIs
Fixes#10674Fixes#10675
Upon moving the version string from PKG_VERSION and Cargo.toml, we lost
the bump version automation. To avoid more bugs here in the future, we
now check for the version marker across all Git-tracked files,
regardless of their extension.
Fixes#10748
---------
Co-authored-by: Thomas Eizinger <thomas@eizinger.io>
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
Apparently if we set the CFBundleDisplayName we hint by default that
we *do* want to show it on newer macOS versions.
This seems to have been uncovered by Xcode 26 build recently.
Fixes#10579
Quality of life improvements for macOS devs, mostly relevant when not
using Xcode as daily driver - although some convenience functions &
explicit sentry dependency should make it better there too.
When working on the Swift codebase, I noticed that running the formatter
produced a massive diff. This PR re-formats the Swift code with `swift
format . --recursive --in-place` and adds a CI check to enforce it going
forward.
Resolves: #9534
---------
Co-authored-by: Jamil Bou Kheir <jamilbk@users.noreply.github.com>
On Apple platforms, `UserDefaults` provides a convenient way to store
and fetch simple plist-compatible data for your app. Unbeknownst to the
author at the time of original implementation was the fact this these
keys are already designed for managed configurations to "mask" any
user-configured equivalents.
This means we no longer need to juggle two dicts in UserDefaults, and we
can instead check which keys are forced via a simple method call.
Additionally, the implementation was simplified in the following ways:
- The host app is the "source of truth" for app configuration now. The
tunnel service receives `setConfiguration` which applies the current
configuration, and saves it in order to start up again without the GUI
connected. The obvious caveat here is that if the GUI isn't running,
configuration such as `internetResourceEnabled` applied by the
administrator won't take effect. This is considered an edge case for the
time being since no customers have asked for this. Additionally, admins
can be advised to ensure the Firezone GUI is running on the system at
all times to prevent this.
- Settings and ConfigurationManager are now able to be removed - these
became redundant after consolidating configuration to the containing
app.
We are currently storing app configuration across three places:
- UserDefaults (favorite resources)
- VPN configuration (Settings)
- Disk (firezone id)
These can be consolidated to UserDefaults, which is the standard way to
store app configuration like this.
UserDefaults is the umbrella persistence store for regular app
configuration (`plist` files which are just XML dictionaries),
iCloud-synced app configuration across a user's devices, and managed app
configuration (MDM). They provide a cached, thread-safe, and
interprocess-supported mechanism for handling app configuration. We can
also subscribe to changes on this app configuration to react to changes.
Unfortunately, the System Extension ruins some of our fun because it
runs as root, and is confined to a different group container, meaning we
cannot share configuration directly between GUI and tunnel procs.
To address this, we use the tunnel process to store all vital
configuration and introduce IPC calls to set and fetch these.
Commit-by-commit review recommended, but things got a little crazy
towards the end when I realized that we can't share a single
UserDefaults between both procs.
The current `rust/` directory is a bit of a wild-west in terms of how
the crates are organised. Most of them are simply at the top-level when
in reality, they are all `connlib`-related. The Apple and Android FFI
crates - which are entrypoints in the Rust code are defined several
layers deep.
To improve the situation, we move around and rename several crates. The
end result is that all top-level crates / directories are:
- Either entrypoints into the Rust code, i.e. applications such as
Gateway, Relay or a Client
- Or crates shared across all those entrypoints, such as `telemetry` or
`logging`
When developing the macOS app, we always build the exact same version
and build code for each build. ~~This _may_ be one reason why we
constantly have to deactivate the extension before the new one will
launch.~~ Edit: Just tested, and I can verify that this does fix the
issue on dev builds, so no more having to uninstall the sysex between
builds.
Even if that's not the reason, this is a cleaner approach than building
it in our prod-only scripts.
---------
Signed-off-by: Jamil <jamilbk@users.noreply.github.com>
Adds SwiftLint using (mostly) default rules to our build pipeline.
Unfortunately, this results in quite a lot of errors / warnings.
All fixes included in this PR are simply refactoring. No functionality
has been changed.
Fixes: #4302
Xcode doesn't allow wildcards in input file lists, so the rules I set up
in #7488 never took effect.
Upon further investigation, it appears that the `strip` command executed
unconditionally at the end of every Rust build was the culprit. Since
Xcode already does this for us, it's a useless step that adds about 30s
to the build time.
Unfortunately there isn't a good way to tell Xcode not to build rust.
But now we don't need to -- `cargo`'s build cache is smart enough to
skip builds and we are back to the ~1-2s range for repeated builds when
only Swift code has changed.
We also add the swift bridge generated code to version control. These
doesn't change regularly, and Xcode sometimes complains that the files
don't exist _before_ it lets you run the `cargo build` to generate them
🙃 .