mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 18:18:55 +00:00
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 #10674
Fixes #10675
153 lines
4.7 KiB
Swift
153 lines
4.7 KiB
Swift
//
|
|
// NetworkSettings.swift
|
|
// (c) 2024 Firezone, Inc.
|
|
// LICENSE: Apache-2.0
|
|
|
|
import FirezoneKit
|
|
import Foundation
|
|
import NetworkExtension
|
|
import os.log
|
|
|
|
class NetworkSettings {
|
|
// WireGuard has an 80-byte overhead. We could try setting tunnelOverheadBytes
|
|
// but that's not a reliable way to calculate how big our packets should be,
|
|
// so just use the minimum.
|
|
let mtu: NSNumber = 1280
|
|
|
|
// These will only be initialized once and then don't change
|
|
private weak var packetTunnelProvider: NEPacketTunnelProvider?
|
|
|
|
// Modifiable values
|
|
public var tunnelAddressIPv4: String?
|
|
public var tunnelAddressIPv6: String?
|
|
public var dnsAddresses: [String] = []
|
|
public var routes4: [NEIPv4Route] = []
|
|
public var routes6: [NEIPv6Route] = []
|
|
|
|
// Private to ensure we append the search domain if we set it.
|
|
private var matchDomains: [String] = [""]
|
|
private var searchDomains: [String] = [""]
|
|
|
|
init(packetTunnelProvider: PacketTunnelProvider?) {
|
|
self.packetTunnelProvider = packetTunnelProvider
|
|
}
|
|
|
|
func setSearchDomain(domain: String?) {
|
|
guard let domain = domain else {
|
|
self.matchDomains = [""]
|
|
self.searchDomains = [""]
|
|
return
|
|
}
|
|
|
|
self.matchDomains = ["", domain]
|
|
self.searchDomains = [domain]
|
|
}
|
|
|
|
func setDummyMatchDomain() {
|
|
self.matchDomains = ["firezone-fd0020211111"]
|
|
}
|
|
|
|
func clearDummyMatchDomain() {
|
|
self.matchDomains = [""]
|
|
|
|
self.matchDomains.append(contentsOf: self.searchDomains)
|
|
}
|
|
|
|
func apply(completionHandler: (@Sendable () -> Void)? = nil) {
|
|
// We don't really know the connlib gateway IP address at this point, but just using 127.0.0.1 is okay
|
|
// because the OS doesn't really need this IP address.
|
|
// NEPacketTunnelNetworkSettings taking in tunnelRemoteAddress is probably a bad abstraction caused by
|
|
// NEPacketTunnelNetworkSettings inheriting from NETunnelNetworkSettings.
|
|
let tunnelNetworkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
|
|
|
|
// Set tunnel addresses and routes
|
|
let ipv4Settings = NEIPv4Settings(
|
|
addresses: [tunnelAddressIPv4!], subnetMasks: ["255.255.255.255"])
|
|
// This is a hack since macos routing table ignores, for full route, any prefix smaller than 120.
|
|
// Without this, adding a full route, remove the previous default route and leaves the system with none,
|
|
// completely breaking IPv6 on the user's system.
|
|
let ipv6Settings = NEIPv6Settings(addresses: [tunnelAddressIPv6!], networkPrefixLengths: [120])
|
|
let dnsSettings = NEDNSSettings(servers: dnsAddresses)
|
|
ipv4Settings.includedRoutes = routes4
|
|
ipv6Settings.includedRoutes = routes6
|
|
dnsSettings.matchDomains = matchDomains
|
|
dnsSettings.searchDomains = searchDomains
|
|
dnsSettings.matchDomainsNoSearch = false
|
|
tunnelNetworkSettings.ipv4Settings = ipv4Settings
|
|
tunnelNetworkSettings.ipv6Settings = ipv6Settings
|
|
tunnelNetworkSettings.dnsSettings = dnsSettings
|
|
tunnelNetworkSettings.mtu = mtu
|
|
|
|
packetTunnelProvider?.setTunnelNetworkSettings(tunnelNetworkSettings) { error in
|
|
if let error = error {
|
|
Log.error(error)
|
|
}
|
|
|
|
completionHandler?()
|
|
}
|
|
}
|
|
}
|
|
|
|
// For creating IPv4 routes
|
|
enum IPv4SubnetMaskLookup {
|
|
static let table: [Int: String] = [
|
|
0: "0.0.0.0",
|
|
1: "128.0.0.0",
|
|
2: "192.0.0.0",
|
|
3: "224.0.0.0",
|
|
4: "240.0.0.0",
|
|
5: "248.0.0.0",
|
|
6: "252.0.0.0",
|
|
7: "254.0.0.0",
|
|
8: "255.0.0.0",
|
|
9: "255.128.0.0",
|
|
10: "255.192.0.0",
|
|
11: "255.224.0.0",
|
|
12: "255.240.0.0",
|
|
13: "255.248.0.0",
|
|
14: "255.252.0.0",
|
|
15: "255.254.0.0",
|
|
16: "255.255.0.0",
|
|
17: "255.255.128.0",
|
|
18: "255.255.192.0",
|
|
19: "255.255.224.0",
|
|
20: "255.255.240.0",
|
|
21: "255.255.248.0",
|
|
22: "255.255.252.0",
|
|
23: "255.255.254.0",
|
|
24: "255.255.255.0",
|
|
25: "255.255.255.128",
|
|
26: "255.255.255.192",
|
|
27: "255.255.255.224",
|
|
28: "255.255.255.240",
|
|
29: "255.255.255.248",
|
|
30: "255.255.255.252",
|
|
31: "255.255.255.254",
|
|
32: "255.255.255.255",
|
|
]
|
|
}
|
|
|
|
// Route convenience helpers.
|
|
extension NetworkSettings {
|
|
struct Cidr {
|
|
let address: String
|
|
let prefix: Int
|
|
|
|
var asNEIPv4Route: NEIPv4Route? {
|
|
guard let subnetMask = IPv4SubnetMaskLookup.table[prefix] else {
|
|
Log.warning("Invalid IPv4 prefix: \(prefix) for address: \(address)")
|
|
return nil
|
|
}
|
|
return NEIPv4Route(destinationAddress: address, subnetMask: subnetMask)
|
|
}
|
|
|
|
var asNEIPv6Route: NEIPv6Route? {
|
|
guard prefix >= 0 && prefix <= 128 else {
|
|
Log.warning("Invalid IPv6 prefix: \(prefix) for address: \(address)")
|
|
return nil
|
|
}
|
|
return NEIPv6Route(destinationAddress: address, networkPrefixLength: NSNumber(value: prefix))
|
|
}
|
|
}
|
|
}
|