Refactor DNUpdater to an actor and try to fix warnings

This commit is contained in:
Caleb Jasik 2025-02-19 15:51:47 -06:00
parent f59ab7c09d
commit be56774197
No known key found for this signature in database
4 changed files with 44 additions and 45 deletions

View file

@ -23,7 +23,7 @@ extension AppMessageError: LocalizedError {
} }
} }
class PacketTunnelProvider: NEPacketTunnelProvider { final class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
private var networkMonitor: NWPathMonitor? private var networkMonitor: NWPathMonitor?
private var site: Site? private var site: Site?
@ -92,8 +92,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
throw err! throw err!
} }
tunnelNetworkSettings.ipv4Settings = NEIPv4Settings( tunnelNetworkSettings.ipv4Settings = NEIPv4Settings(
addresses: [ipNet!.ip], subnetMasks: [ipNet!.maskCIDR] addresses: [ipNet!.ip], subnetMasks: [ipNet!.maskCIDR])
)
var routes: [NEIPv4Route] = [ var routes: [NEIPv4Route] = [
NEIPv4Route(destinationAddress: ipNet!.network, subnetMask: ipNet!.maskCIDR) NEIPv4Route(destinationAddress: ipNet!.network, subnetMask: ipNet!.maskCIDR)
] ]
@ -113,8 +112,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
try await setTunnelNetworkSettings(tunnelNetworkSettings) try await setTunnelNetworkSettings(tunnelNetworkSettings)
var nebulaErr: NSError? var nebulaErr: NSError?
nebula = MobileNebulaNewNebula( nebula = MobileNebulaNewNebula(
String(data: config, encoding: .utf8), key, site!.logFile, tunFD, &nebulaErr String(data: config, encoding: .utf8), key, site!.logFile, tunFD, &nebulaErr)
)
startNetworkMonitor() startNetworkMonitor()
if nebulaErr != nil { if nebulaErr != nil {
@ -123,15 +121,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
} }
nebula!.start() nebula!.start()
dnUpdater.updateSingleLoop(site: site!, onUpdate: handleDNUpdate) await dnUpdater.updateSingleLoop(site: site!, onUpdate: handleDNUpdate)
} }
private func handleDNUpdate(newSite: Site) { private func handleDNUpdate(newSite: Site) {
do { do {
site = newSite site = newSite
try nebula?.reload( try nebula?.reload(String(data: newSite.getConfig(), encoding: .utf8), key: newSite.getKey())
String(data: newSite.getConfig(), encoding: .utf8), key: newSite.getKey()
)
} catch { } catch {
log.error( log.error(
@ -180,9 +176,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
networkMonitor = nil networkMonitor = nil
} }
override func stopTunnel( override func stopTunnel(with _: NEProviderStopReason, completionHandler: @escaping () -> Void) {
with _: NEProviderStopReason, completionHandler: @escaping () -> Void
) {
nebula?.stop() nebula?.stop()
stopNetworkMonitor() stopNetworkMonitor()
completionHandler() completionHandler()
@ -259,9 +253,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
if error != nil { if error != nil {
return try? JSONEncoder().encode( return try? JSONEncoder().encode(
IPCResponse( IPCResponse(type: .error, message: JSON(error?.localizedDescription ?? "Unknown error")))
type: .error, message: JSON(error?.localizedDescription ?? "Unknown error")
))
} else { } else {
return try? JSONEncoder().encode(IPCResponse(type: .success, message: data)) return try? JSONEncoder().encode(IPCResponse(type: .success, message: data))
} }
@ -276,14 +268,16 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
private func getHostInfo(args: JSON) -> (JSON?, (any Error)?) { private func getHostInfo(args: JSON) -> (JSON?, (any Error)?) {
var err: NSError? var err: NSError?
let res = nebula!.getHostInfo( 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) return (JSON(res), err)
} }
private func setRemoteForTunnel(args: JSON) -> (JSON?, (any Error)?) { private func setRemoteForTunnel(args: JSON) -> (JSON?, (any Error)?) {
var err: NSError? var err: NSError?
let res = nebula!.setRemoteForTunnel( 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) return (JSON(res), err)
} }

View file

@ -150,7 +150,7 @@ let statusString: [NEVPNStatus: String] = [
] ]
// Represents a site that was pulled out of the system configuration // Represents a site that was pulled out of the system configuration
class Site: Codable { final class Site: Codable, @unchecked Sendable {
// Stored in manager // Stored in manager
var name: String var name: String
var id: 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") 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)") 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") 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 // 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 name: String
var id: String var id: String
var staticHostmap: [String: StaticHosts] var staticHostmap: [String: StaticHosts]

View file

@ -25,17 +25,19 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
) -> Bool { ) -> Bool {
GeneratedPluginRegistrant.register(with: self) GeneratedPluginRegistrant.register(with: self)
dnUpdater.updateAllLoop { site in Task {
// Signal the site has changed in case the current site details screen is active await dnUpdater.updateAllLoop { @MainActor site in
let container = self.sites?.getContainer(id: site.id) // Signal the site has changed in case the current site details screen is active
if container != nil { let container = self.sites?.getContainer(id: site.id)
// Update references to the site with the new site config if container != nil {
container!.site = site // Update references to the site with the new site config
container!.updater.update(connected: site.connected ?? false, replaceSite: site) container!.site = site
} container!.updater.update(connected: site.connected ?? false, replaceSite: site)
}
// Signal to the main screen to reload // Signal to the main screen to reload
self.ui?.invokeMethod("refreshSites", arguments: nil) self.ui?.invokeMethod("refreshSites", arguments: nil)
}
} }
guard let controller = window?.rootViewController as? FlutterViewController else { guard let controller = window?.rootViewController as? FlutterViewController else {
@ -86,8 +88,7 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
if err != nil { if err != nil {
return result( return result(
CallFailedError( CallFailedError(
message: "Error while parsing certificate(s)", details: err!.localizedDescription message: "Error while parsing certificate(s)", details: err!.localizedDescription))
))
} }
return result(json) return result(json)
@ -109,8 +110,7 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
return result( return result(
CallFailedError( CallFailedError(
message: "Error while verifying certificate and private key", message: "Error while verifying certificate and private key",
details: err!.localizedDescription details: err!.localizedDescription))
))
} }
return result(valid) return result(valid)
@ -122,8 +122,7 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
if err != nil { if err != nil {
return result( return result(
CallFailedError( CallFailedError(
message: "Error while generating key pairs", details: err!.localizedDescription message: "Error while generating key pairs", details: err!.localizedDescription))
))
} }
return result(kp) return result(kp)
@ -241,8 +240,7 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
} catch { } catch {
return result( return result(
CallFailedError( CallFailedError(
message: "Could not start site", details: error.localizedDescription message: "Could not start site", details: error.localizedDescription))
))
} }
} }
} }

View file

@ -1,29 +1,33 @@
import Foundation import Foundation
import os.log import os.log
class DNUpdater { actor DNUpdater {
private let apiClient = APIClient() private let apiClient = APIClient()
private let timer = RepeatingTimer(timeInterval: 15 * 60) // 15 * 60 is 15 minutes private let timer = RepeatingTimer(timeInterval: 15 * 60) // 15 * 60 is 15 minutes
private let log = Logger(subsystem: "net.defined.mobileNebula", category: "DNUpdater") 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 _ = 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 // 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. // could block for a while. Push ourselves onto another thread to avoid blocking the UI.
Task.detached(priority: .userInitiated) { Task.detached(priority: .userInitiated) {
sites?.values.forEach { site in for site in unwrappedSites.values {
if site.connected == true { if site.connected == true {
// The vpn service is in charge of updating the currently connected site // The vpn service is in charge of updating the currently connected site
return 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 = { timer.eventHandler = {
self.updateAll(onUpdate: onUpdate) self.updateAll(onUpdate: onUpdate)
} }
@ -37,7 +41,7 @@ class DNUpdater {
timer.resume() timer.resume()
} }
func updateSite(site: Site, onUpdate: @escaping (Site) -> Void) { func updateSite(site: Site, onUpdate: sending @escaping (Site) -> Void) {
do { do {
if !site.managed { if !site.managed {
return return