diff --git a/ios/NebulaNetworkExtension/PacketTunnelProvider.swift b/ios/NebulaNetworkExtension/PacketTunnelProvider.swift index e587e43..5777d84 100644 --- a/ios/NebulaNetworkExtension/PacketTunnelProvider.swift +++ b/ios/NebulaNetworkExtension/PacketTunnelProvider.swift @@ -23,7 +23,7 @@ extension AppMessageError: LocalizedError { } } -class PacketTunnelProvider: NEPacketTunnelProvider { +final class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { private var networkMonitor: NWPathMonitor? private var site: Site? @@ -92,8 +92,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { throw err! } tunnelNetworkSettings.ipv4Settings = NEIPv4Settings( - addresses: [ipNet!.ip], subnetMasks: [ipNet!.maskCIDR] - ) + addresses: [ipNet!.ip], subnetMasks: [ipNet!.maskCIDR]) var routes: [NEIPv4Route] = [ NEIPv4Route(destinationAddress: ipNet!.network, subnetMask: ipNet!.maskCIDR) ] @@ -113,8 +112,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { try await setTunnelNetworkSettings(tunnelNetworkSettings) var nebulaErr: NSError? nebula = MobileNebulaNewNebula( - String(data: config, encoding: .utf8), key, site!.logFile, tunFD, &nebulaErr - ) + String(data: config, encoding: .utf8), key, site!.logFile, tunFD, &nebulaErr) startNetworkMonitor() if nebulaErr != nil { @@ -123,15 +121,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider { } nebula!.start() - dnUpdater.updateSingleLoop(site: site!, onUpdate: handleDNUpdate) + await dnUpdater.updateSingleLoop(site: site!, onUpdate: handleDNUpdate) } private func handleDNUpdate(newSite: Site) { do { site = newSite - try nebula?.reload( - String(data: newSite.getConfig(), encoding: .utf8), key: newSite.getKey() - ) + try nebula?.reload(String(data: newSite.getConfig(), encoding: .utf8), key: newSite.getKey()) } catch { log.error( @@ -180,9 +176,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { networkMonitor = nil } - override func stopTunnel( - with _: NEProviderStopReason, completionHandler: @escaping () -> Void - ) { + override func stopTunnel(with _: NEProviderStopReason, completionHandler: @escaping () -> Void) { nebula?.stop() stopNetworkMonitor() completionHandler() @@ -259,9 +253,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { if error != nil { return try? JSONEncoder().encode( - IPCResponse( - type: .error, message: JSON(error?.localizedDescription ?? "Unknown error") - )) + IPCResponse(type: .error, message: JSON(error?.localizedDescription ?? "Unknown error"))) } else { return try? JSONEncoder().encode(IPCResponse(type: .success, message: data)) } @@ -276,14 +268,16 @@ class PacketTunnelProvider: NEPacketTunnelProvider { private func getHostInfo(args: JSON) -> (JSON?, (any Error)?) { var err: NSError? let res = nebula!.getHostInfo( - byVpnIp: args["vpnIp"].string, pending: args["pending"].boolValue, error: &err) + byVpnIp: args["vpnIp"].string, pending: args["pending"].boolValue, error: &err + ) return (JSON(res), err) } private func setRemoteForTunnel(args: JSON) -> (JSON?, (any Error)?) { var err: NSError? let res = nebula!.setRemoteForTunnel( - args["vpnIp"].string, addr: args["addr"].string, error: &err) + args["vpnIp"].string, addr: args["addr"].string, error: &err + ) return (JSON(res), err) } diff --git a/ios/NebulaNetworkExtension/Site.swift b/ios/NebulaNetworkExtension/Site.swift index 1592bd8..52d3fc6 100644 --- a/ios/NebulaNetworkExtension/Site.swift +++ b/ios/NebulaNetworkExtension/Site.swift @@ -150,7 +150,7 @@ let statusString: [NEVPNStatus: String] = [ ] // Represents a site that was pulled out of the system configuration -class Site: Codable { +final class Site: Codable, @unchecked Sendable { // Stored in manager var name: String var id: String @@ -278,7 +278,10 @@ class Site: Codable { } } - if hasErrors && !managed { + if hasErrors, !managed { + errors.append("There are issues with 1 or more ca certificates") + } + if hasErrors, !managed { errors.append("There are issues with 1 or more ca certificates") } @@ -294,7 +297,7 @@ class Site: Codable { errors.append("Unable to create the site directory: \(error.localizedDescription)") } - if managed && (try? getDNCredentials())?.invalid != false { + if managed, (try? getDNCredentials())?.invalid != false { errors.append("Unable to fetch managed updates - please re-enroll the device") } @@ -426,7 +429,7 @@ class DNCredentials: Codable { } // This class represents a site coming in from flutter, meant only to be saved and re-loaded as a proper Site -struct IncomingSite: Codable { +struct IncomingSite: Codable, @unchecked Sendable { var name: String var id: String var staticHostmap: [String: StaticHosts] diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index f6c6084..c464394 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -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 { + await dnUpdater.updateAllLoop { @MainActor 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) + } - // 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 { @@ -86,8 +88,7 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError { if err != nil { return result( CallFailedError( - message: "Error while parsing certificate(s)", details: err!.localizedDescription - )) + message: "Error while parsing certificate(s)", details: err!.localizedDescription)) } return result(json) @@ -109,8 +110,7 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError { return result( CallFailedError( message: "Error while verifying certificate and private key", - details: err!.localizedDescription - )) + details: err!.localizedDescription)) } return result(valid) @@ -122,8 +122,7 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError { if err != nil { return result( CallFailedError( - message: "Error while generating key pairs", details: err!.localizedDescription - )) + message: "Error while generating key pairs", details: err!.localizedDescription)) } return result(kp) @@ -241,8 +240,7 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError { } catch { return result( CallFailedError( - message: "Could not start site", details: error.localizedDescription - )) + message: "Could not start site", details: error.localizedDescription)) } } } diff --git a/ios/Runner/DNUpdate.swift b/ios/Runner/DNUpdate.swift index 53d7d2c..1739708 100644 --- a/ios/Runner/DNUpdate.swift +++ b/ios/Runner/DNUpdate.swift @@ -1,29 +1,33 @@ import Foundation import os.log -class DNUpdater { +actor DNUpdater { 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, _ in + guard let unwrappedSites = sites else { + // There was an error, let's bail. + return + } // 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. Task.detached(priority: .userInitiated) { - sites?.values.forEach { site in + for site in unwrappedSites.values { if site.connected == true { // The vpn service is in charge of updating the currently connected site return } - self.updateSite(site: site, onUpdate: onUpdate) + await self.updateSite(site: site, onUpdate: onUpdate) } } } } - func updateAllLoop(onUpdate: @escaping (Site) -> Void) { + func updateAllLoop(onUpdate: @Sendable @escaping (Site) -> Void) { timer.eventHandler = { self.updateAll(onUpdate: onUpdate) } @@ -37,7 +41,7 @@ class DNUpdater { timer.resume() } - func updateSite(site: Site, onUpdate: @escaping (Site) -> Void) { + func updateSite(site: Site, onUpdate: sending @escaping (Site) -> Void) { do { if !site.managed { return