Work on making DNUpdate use an async timer implementation

This commit is contained in:
Caleb Jasik 2025-02-12 15:23:20 -06:00
parent e4940d3e3a
commit 8ee9979b16
No known key found for this signature in database
4 changed files with 93 additions and 72 deletions

View file

@ -1,7 +1,7 @@
import NetworkExtension
import MobileNebula import MobileNebula
import os.log import NetworkExtension
import SwiftyJSON import SwiftyJSON
import os.log
enum VPNStartError: Error { enum VPNStartError: Error {
case noManagers case noManagers
@ -23,7 +23,6 @@ extension AppMessageError: LocalizedError {
} }
} }
// FIXME: marked as unchecked Sendable to allow sending `self.pathUpdate`, but we should refactor and re-enable linting. // FIXME: marked as unchecked Sendable to allow sending `self.pathUpdate`, but we should refactor and re-enable linting.
class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
private var networkMonitor: NWPathMonitor? private var networkMonitor: NWPathMonitor?
@ -56,7 +55,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
do { do {
// Cannot use NETunnelProviderManager.loadAllFromPreferences() in earlier versions of iOS // Cannot use NETunnelProviderManager.loadAllFromPreferences() in earlier versions of iOS
// TODO: Remove else once we drop support for iOS 16 // TODO: Remove else once we drop support for iOS 16
if ProcessInfo().isOperatingSystemAtLeast(OperatingSystemVersion(majorVersion: 17, minorVersion: 0, patchVersion: 0)) { if ProcessInfo().isOperatingSystemAtLeast(
OperatingSystemVersion(majorVersion: 17, minorVersion: 0, patchVersion: 0))
{
manager = try await self.findManager() manager = try await self.findManager()
guard let foundManager = manager else { guard let foundManager = manager else {
throw VPNStartError.couldNotFindManager throw VPNStartError.couldNotFindManager
@ -88,7 +89,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
// Make sure our ip is routed to the tun device // Make sure our ip is routed to the tun device
var err: NSError? var err: NSError?
let ipNet = MobileNebulaParseCIDR(_site.cert!.cert.details.ips[0], &err) let ipNet = MobileNebulaParseCIDR(_site.cert!.cert.details.ips[0], &err)
if (err != nil) { if err != nil {
throw err! throw err!
} }
tunnelNetworkSettings.ipv4Settings = NEIPv4Settings(addresses: [ipNet!.ip], subnetMasks: [ipNet!.maskCIDR]) tunnelNetworkSettings.ipv4Settings = NEIPv4Settings(addresses: [ipNet!.ip], subnetMasks: [ipNet!.maskCIDR])
@ -97,7 +98,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
// Add our unsafe routes // Add our unsafe routes
try _site.unsafeRoutes.forEach { unsafeRoute in try _site.unsafeRoutes.forEach { unsafeRoute in
let ipNet = MobileNebulaParseCIDR(unsafeRoute.route, &err) let ipNet = MobileNebulaParseCIDR(unsafeRoute.route, &err)
if (err != nil) { if err != nil {
throw err! throw err!
} }
routes.append(NEIPv4Route(destinationAddress: ipNet!.network, subnetMask: ipNet!.maskCIDR)) routes.append(NEIPv4Route(destinationAddress: ipNet!.network, subnetMask: ipNet!.maskCIDR))
@ -108,7 +109,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
try await self.setTunnelNetworkSettings(tunnelNetworkSettings) try await self.setTunnelNetworkSettings(tunnelNetworkSettings)
var nebulaErr: NSError? var nebulaErr: NSError?
self.nebula = MobileNebulaNewNebula(String(data: config, encoding: .utf8), key, self.site!.logFile, tunFD, &nebulaErr) self.nebula = MobileNebulaNewNebula(
String(data: config, encoding: .utf8), key, self.site!.logFile, tunFD, &nebulaErr)
self.startNetworkMonitor() self.startNetworkMonitor()
if nebulaErr != nil { if nebulaErr != nil {
@ -151,7 +153,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
throw VPNStartError.noProviderConfig throw VPNStartError.noProviderConfig
} }
let id = mgrProviderConfig["id"] as? String let id = mgrProviderConfig["id"] as? String
if (id == targetID) { if id == targetID {
return manager return manager
} }
} }
@ -223,7 +225,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
defer { defer {
self.cancelTunnelWithError(error) self.cancelTunnelWithError(error)
} }
return try? JSONEncoder().encode(IPCResponse.init(type: .error, message: JSON(error.localizedDescription))) return try? JSONEncoder().encode(
IPCResponse.init(type: .error, message: JSON(error.localizedDescription)))
} }
} }
@ -245,8 +248,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
error = AppMessageError.unknownIPCType(command: call.command) error = AppMessageError.unknownIPCType(command: call.command)
} }
if (error != nil) { if error != nil {
return try? JSONEncoder().encode(IPCResponse.init(type: .error, message: JSON(error?.localizedDescription ?? "Unknown error"))) return try? JSONEncoder().encode(
IPCResponse.init(type: .error, message: JSON(error?.localizedDescription ?? "Unknown error")))
} else { } else {
return try? JSONEncoder().encode(IPCResponse.init(type: .success, message: data)) return try? JSONEncoder().encode(IPCResponse.init(type: .success, message: data))
} }
@ -307,4 +311,3 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
return nil return nil
} }
} }

View file

@ -56,6 +56,26 @@ class SiteList {
#endif #endif
} }
static func loadAllAsync() async -> Result<[String: Site], any Error> {
await withCheckedContinuation { continuation in
#if targetEnvironment(simulator)
SiteList.loadAllFromFS { sites, err in
if err != nil {
continuation.resume(returning: Result.failure(err!))
}
continuation.resume(returning: Result.success(sites!))
}
#else
SiteList.loadAllFromNETPM { sites, err in
if err != nil {
continuation.resume(returning: Result.failure(err!))
}
continuation.resume(returning: Result.success(sites!))
}
#endif
}
}
private static func loadAllFromFS(completion: @escaping ([String: Site]?, (any Error)?) -> Void) { private static func loadAllFromFS(completion: @escaping ([String: Site]?, (any Error)?) -> Void) {
let fileManager = FileManager.default let fileManager = FileManager.default
var siteDirs: [URL] var siteDirs: [URL]

View file

@ -26,15 +26,13 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
GeneratedPluginRegistrant.register(with: self) GeneratedPluginRegistrant.register(with: self)
Task { Task {
for await site in dnUpdater.siteUpdates { for await site in await dnUpdater.siteUpdates {
self.sites?.updateSite(site: site) self.sites?.updateSite(site: site)
// Send the refresh sites command on the main thread
DispatchQueue.main.async {
// 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 {
fatalError("rootViewController is not type FlutterViewController") fatalError("rootViewController is not type FlutterViewController")

View file

@ -1,7 +1,7 @@
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")
@ -18,7 +18,7 @@ class DNUpdater {
return return
} }
self.updateSite(site: site, onUpdate: onUpdate) await self.updateSite(site: site, onUpdate: onUpdate)
} }
} }
@ -28,6 +28,20 @@ class DNUpdater {
}) })
} }
// 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
timer.eventHandler = {
self.updateAll(onUpdate: { site in
continuation.yield(site)
})
}
timer.resume()
}
}
func updateAllLoop(onUpdate: @Sendable @escaping (Site) -> Void) { func updateAllLoop(onUpdate: @Sendable @escaping (Site) -> Void) {
timer.eventHandler = { timer.eventHandler = {
self.updateAll(onUpdate: onUpdate) self.updateAll(onUpdate: onUpdate)
@ -95,20 +109,6 @@ class DNUpdater {
} }
} }
extension DNUpdater {
// 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)
})
}
}
}
// From https://medium.com/over-engineering/a-background-repeating-timer-in-swift-412cecfd2ef9 // From https://medium.com/over-engineering/a-background-repeating-timer-in-swift-412cecfd2ef9
class RepeatingTimer { class RepeatingTimer {