Compare commits

...

10 commits

Author SHA1 Message Date
Caleb Jasik
a7f1db60ae
Merge 281d690ef3 into de2b3dccb7 2025-07-31 10:38:53 -05:00
John Maguire
de2b3dccb7
Enable tunnels.drop_inactive with 10m timeout (#287) 2025-07-29 16:29:02 -04:00
Caleb Jasik
281d690ef3
Use Swift 6 2025-03-27 12:35:12 -05:00
Caleb Jasik
a32d17705c
Use an AsyncStream to avoid sending @MainActor restricted AppDelegate into other isolation domains 2025-03-27 12:35:12 -05:00
Caleb Jasik
df4c3a51b8
Mark closures in DNUpdate as @Sendable 2025-03-27 12:35:12 -05:00
Caleb Jasik
8e9b5fcc4a
Mark PacketTunnelProvider as @unchecked Sendable 2025-03-27 12:35:12 -05:00
Caleb Jasik
f3882997be
Convert dict to Sendable string before throwing 2025-03-27 12:35:12 -05:00
Caleb Jasik
bc67c06ef7
Mark SiteUpdater as @unchecked Sendable 2025-03-27 12:35:12 -05:00
Caleb Jasik
500e49edc3
Mark Flutter import as @preconcurrency 2025-03-27 12:35:12 -05:00
Caleb Jasik
ae34c59456
Enable StrictConcurrency=complete swift feature flag
<https://github.com/swiftlang/swift-evolution/blob/main/proposals/0337-support-incremental-migration-to-concurrency-checking.md>

This causes several warnings. I'm going to start by trying to silence them by reflecting the current state of the project, rather than fixing them directly.
2025-03-27 12:35:11 -05:00
8 changed files with 56 additions and 29 deletions

View file

@ -23,7 +23,7 @@ extension AppMessageError: LocalizedError {
}
}
class PacketTunnelProvider: NEPacketTunnelProvider {
class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
private var networkMonitor: NWPathMonitor?
private var site: Site?

View file

@ -6,7 +6,7 @@ import os.log
let log = Logger(subsystem: "net.defined.mobileNebula", category: "Site")
enum SiteError: Error {
case nonConforming(site: [String: Any]?)
case nonConforming(site: String)
case noCertificate
case keyLoad
case keySave
@ -22,7 +22,7 @@ extension SiteError: CustomStringConvertible {
public var description: String {
switch self {
case .nonConforming(let site):
return String("Non-conforming site \(String(describing: site))")
return String("Non-conforming site \(site)")
case .noCertificate:
return "No certificate found"
case .keyLoad:
@ -150,7 +150,7 @@ let statusString: [NEVPNStatus: String] = [
]
// Represents a site that was pulled out of the system configuration
class Site: Codable {
class Site: Codable, @unchecked Sendable {
// Stored in manager
var name: String
var id: String
@ -208,7 +208,7 @@ class Site: Codable {
let id = dict?["id"] as? String ?? nil
if id == nil {
throw SiteError.nonConforming(site: dict)
throw SiteError.nonConforming(site: String(describing: dict))
}
try self.init(path: SiteList.getSiteConfigFile(id: id!, createDir: false))

View file

@ -563,6 +563,7 @@
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE = YES;
SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN = YES;
SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES;
@ -574,7 +575,7 @@
SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES;
SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES = YES;
SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION = YES;
SWIFT_VERSION = 5.0;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
@ -784,6 +785,7 @@
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE = YES;
SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN = YES;
SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES;
@ -795,7 +797,7 @@
SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES;
SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES = YES;
SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION = YES;
SWIFT_VERSION = 5.0;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
@ -849,6 +851,7 @@
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE = YES;
SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN = YES;
SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES;
@ -860,7 +863,7 @@
SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES;
SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES = YES;
SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION = YES;
SWIFT_VERSION = 5.0;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};

View file

@ -1,10 +1,10 @@
import MobileNebula
@preconcurrency import MobileNebula
enum APIClientError: Error {
case invalidCredentials
}
class APIClient {
struct APIClient: Sendable {
let apiClient: MobileNebulaAPIClient
let json = JSONDecoder()

View file

@ -1,4 +1,4 @@
import Flutter
@preconcurrency import Flutter
import MobileNebula
import NetworkExtension
import SwiftyJSON
@ -25,17 +25,19 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
dnUpdater.updateAllLoop { site in
// Signal the site has changed in case the current site details screen is active
let container = self.sites?.getContainer(id: site.id)
if container != nil {
// Update references to the site with the new site config
container!.site = site
container!.updater.update(connected: site.connected ?? false, replaceSite: site)
}
Task {
for await site in dnUpdater.siteUpdates {
// Signal the site has changed in case the current site details screen is active
let container = self.sites?.getContainer(id: site.id)
if container != nil {
// Update references to the site with the new site config
container!.site = site
container!.updater.update(connected: site.connected ?? false, replaceSite: site)
}
// Signal to the main screen to reload
self.ui?.invokeMethod("refreshSites", arguments: nil)
// Signal to the main screen to reload
self.ui?.invokeMethod("refreshSites", arguments: nil)
}
}
guard let controller = window?.rootViewController as? FlutterViewController else {

View file

@ -1,12 +1,12 @@
import Foundation
import os.log
class DNUpdater {
class DNUpdater: @unchecked Sendable {
private let apiClient = APIClient()
private let timer = RepeatingTimer(timeInterval: 15 * 60) // 15 * 60 is 15 minutes
private let log = Logger(subsystem: "net.defined.mobileNebula", category: "DNUpdater")
func updateAll(onUpdate: @escaping (Site) -> Void) {
func updateAll(onUpdate: @Sendable @escaping (Site) -> Void) {
_ = SiteList { (sites, _) -> Void in
// NEVPN seems to force us onto the main thread and we are about to make network calls that
// could block for a while. Push ourselves onto another thread to avoid blocking the UI.
@ -23,21 +23,33 @@ class DNUpdater {
}
}
func updateAllLoop(onUpdate: @escaping (Site) -> Void) {
func updateAllLoop(onUpdate: @Sendable @escaping (Site) -> Void) {
timer.eventHandler = {
self.updateAll(onUpdate: onUpdate)
}
timer.resume()
}
func updateSingleLoop(site: Site, onUpdate: @escaping (Site) -> Void) {
// Site updates provides an async/await alternative to `.updateAllLoop` that doesn't require a sendable closure.
// https://developer.apple.com/documentation/swift/asyncstream
var siteUpdates: AsyncStream<Site> {
AsyncStream { continuation in
self.updateAllLoop(onUpdate: { site in
continuation.yield(site)
})
}
}
func updateSingleLoop(site: Site, onUpdate: @Sendable @escaping (Site) -> Void) {
timer.eventHandler = {
self.updateSite(site: site, onUpdate: onUpdate)
}
timer.resume()
}
func updateSite(site: Site, onUpdate: @escaping (Site) -> Void) {
func updateSite(site: Site, onUpdate: @Sendable @escaping (Site) -> Void) {
do {
if !site.managed {
return

View file

@ -78,14 +78,14 @@ class Sites {
}
}
class SiteUpdater: NSObject, FlutterStreamHandler {
class SiteUpdater: NSObject, FlutterStreamHandler, @unchecked Sendable {
private var eventSink: FlutterEventSink?
private var eventChannel: FlutterEventChannel
private var site: Site
private var notification: Any?
public var startFunc: (() -> Void)?
private var configFd: Int32? = nil
private var configObserver: (any DispatchSourceFileSystemObject)? = nil
private var configFd: Int32?
private var configObserver: (any DispatchSourceFileSystemObject)?
init(messenger: any FlutterBinaryMessenger, site: Site) {
do {

View file

@ -13,6 +13,7 @@ type config struct {
Logging configLogging `yaml:"logging"`
Stats configStats `yaml:"stats"`
Handshakes configHandshakes `yaml:"handshakes"`
Tunnels configTunnels `yaml:"tunnels"`
Firewall configFirewall `yaml:"firewall"`
Relay configRelay `yaml:"relay"`
}
@ -64,6 +65,10 @@ func newConfig() *config {
Retries: 20,
WaitRotation: 5,
},
Tunnels: configTunnels{
DropInactive: true,
InactivityTimeout: "10m",
},
Firewall: configFirewall{
Conntrack: configConntrack{
TcpTimeout: "120h",
@ -180,6 +185,11 @@ type configHandshakes struct {
WaitRotation int `yaml:"wait_rotation"`
}
type configTunnels struct {
DropInactive bool `yaml:"drop_inactive"`
InactivityTimeout string `yaml:"inactivity_timeout"`
}
type configFirewall struct {
Conntrack configConntrack `yaml:"conntrack"`
Outbound []configFirewallRule `yaml:"outbound"`