276 lines
9.9 KiB
Swift
276 lines
9.9 KiB
Swift
import Foundation
|
|
import Libbox
|
|
import NetworkExtension
|
|
import os.log
|
|
#if os(iOS)
|
|
import WidgetKit
|
|
#endif
|
|
#if os(macOS)
|
|
import CoreLocation
|
|
#endif
|
|
|
|
open class ExtensionProvider: NEPacketTunnelProvider {
|
|
private static let logger = Logger(category: "ExtensionProvider")
|
|
|
|
public private(set) var commandServer: LibboxCommandServer?
|
|
private var platformInterface: ExtensionPlatformInterface!
|
|
public var tunnelOptions: [String: NSObject]?
|
|
private var startOptionsURL: URL?
|
|
|
|
public struct OverridePreferences {
|
|
public var includeAllNetworks: Bool = false
|
|
public var systemProxyEnabled: Bool = true
|
|
public var excludeDefaultRoute: Bool = false
|
|
public var autoRouteUseSubRangesByDefault: Bool = false
|
|
public var excludeAPNsRoute: Bool = false
|
|
}
|
|
|
|
public var overridePreferences: OverridePreferences?
|
|
|
|
private func applyStartOptions(_ options: [String: NSObject]) {
|
|
tunnelOptions = options
|
|
var prefs = OverridePreferences()
|
|
prefs.includeAllNetworks = (options["includeAllNetworks"] as? NSNumber)?.boolValue ?? false
|
|
prefs.systemProxyEnabled = (options["systemProxyEnabled"] as? NSNumber)?.boolValue ?? true
|
|
prefs.excludeDefaultRoute = (options["excludeDefaultRoute"] as? NSNumber)?.boolValue ?? false
|
|
prefs.autoRouteUseSubRangesByDefault = (options["autoRouteUseSubRangesByDefault"] as? NSNumber)?.boolValue ?? false
|
|
prefs.excludeAPNsRoute = (options["excludeAPNsRoute"] as? NSNumber)?.boolValue ?? false
|
|
overridePreferences = prefs
|
|
}
|
|
|
|
private func persistStartOptions(_ options: [String: NSObject]) throws {
|
|
guard let startOptionsURL else {
|
|
return
|
|
}
|
|
let data = try ExtensionStartOptions.encode(options)
|
|
try data.write(to: startOptionsURL, options: .atomic)
|
|
}
|
|
|
|
#if os(macOS)
|
|
private var xpcListener: NSXPCListener?
|
|
private var xpcService: CommandXPCService?
|
|
private var locationManager: CLLocationManager?
|
|
private var locationDelegate: stubLocationDelegate?
|
|
#endif
|
|
|
|
override open func startTunnel(options startOptions: [String: NSObject]?) async throws {
|
|
let basePath: String
|
|
let workingPath: String
|
|
let tempPath: String
|
|
|
|
#if os(macOS)
|
|
if Variant.useSystemExtension {
|
|
let containerURL = FileManager.default.homeDirectoryForCurrentUser
|
|
basePath = containerURL.path
|
|
workingPath = containerURL.appendingPathComponent("Working").path
|
|
tempPath = containerURL.appendingPathComponent("Temp").path
|
|
} else {
|
|
basePath = FilePath.sharedDirectory.relativePath
|
|
workingPath = FilePath.workingDirectory.relativePath
|
|
tempPath = FilePath.cacheDirectory.relativePath
|
|
}
|
|
#else
|
|
basePath = FilePath.sharedDirectory.relativePath
|
|
workingPath = FilePath.workingDirectory.relativePath
|
|
tempPath = FilePath.cacheDirectory.relativePath
|
|
#endif
|
|
|
|
startOptionsURL = URL(fileURLWithPath: basePath).appendingPathComponent(ExtensionStartOptions.snapshotFileName)
|
|
var effectiveOptions = startOptions
|
|
if let startOptions {
|
|
do {
|
|
try persistStartOptions(startOptions)
|
|
} catch {
|
|
throw ExtensionStartupError("(packet-tunnel) error: persist start options: \(error.localizedDescription)")
|
|
}
|
|
} else if let startOptionsURL, FileManager.default.fileExists(atPath: startOptionsURL.path) {
|
|
do {
|
|
let data = try Data(contentsOf: startOptionsURL)
|
|
effectiveOptions = try ExtensionStartOptions.decode(data)
|
|
} catch {
|
|
throw ExtensionStartupError("(packet-tunnel) error: load start options: \(error.localizedDescription)")
|
|
}
|
|
} else {
|
|
throw ExtensionStartupError("(packet-tunnel) error: missing start options")
|
|
}
|
|
|
|
if let effectiveOptions {
|
|
applyStartOptions(effectiveOptions)
|
|
}
|
|
|
|
let options = LibboxSetupOptions()
|
|
options.basePath = basePath
|
|
options.workingPath = workingPath
|
|
options.tempPath = tempPath
|
|
|
|
options.logMaxLines = 3000
|
|
|
|
#if os(tvOS)
|
|
if let port = effectiveOptions?["commandServerPort"] as? NSNumber {
|
|
options.commandServerListenPort = port.int32Value
|
|
}
|
|
if let secret = effectiveOptions?["commandServerSecret"] as? String {
|
|
options.commandServerSecret = secret
|
|
}
|
|
#endif
|
|
|
|
var setupError: NSError?
|
|
LibboxSetup(options, &setupError)
|
|
if let setupError {
|
|
throw ExtensionStartupError("(packet-tunnel) error: setup service: \(setupError.localizedDescription)")
|
|
}
|
|
|
|
let ignoreMemoryLimit = (effectiveOptions?["ignoreMemoryLimit"] as? NSNumber)?.boolValue ?? false
|
|
LibboxSetMemoryLimit(!ignoreMemoryLimit)
|
|
|
|
if platformInterface == nil {
|
|
platformInterface = ExtensionPlatformInterface(self)
|
|
}
|
|
var error: NSError?
|
|
commandServer = LibboxNewCommandServer(platformInterface, platformInterface, &error)
|
|
if let error {
|
|
throw ExtensionStartupError("(packet-tunnel): create command server error: \(error.localizedDescription)")
|
|
}
|
|
do {
|
|
try commandServer!.start()
|
|
} catch {
|
|
throw ExtensionStartupError("(packet-tunnel): start command server error: \(error.localizedDescription)")
|
|
}
|
|
|
|
#if os(macOS)
|
|
if Variant.useSystemExtension {
|
|
let socketPath = options.basePath + "/command.sock"
|
|
xpcService = CommandXPCService(socketPath: socketPath)
|
|
let machServiceName = AppConfiguration.appGroupID + ".system"
|
|
xpcListener = NSXPCListener(machServiceName: machServiceName)
|
|
xpcListener!.delegate = xpcService
|
|
xpcListener!.resume()
|
|
Self.logger.info("set Command Server")
|
|
xpcService!.commandServer = commandServer
|
|
}
|
|
#endif
|
|
|
|
writeMessage("(packet-tunnel): Here I stand")
|
|
try await startService()
|
|
#if os(macOS)
|
|
if Variant.useSystemExtension {
|
|
xpcService!.markServiceReady()
|
|
}
|
|
#endif
|
|
#if os(iOS)
|
|
if #available(iOS 18.0, *) {
|
|
ControlCenter.shared.reloadControls(ofKind: ExtensionProfile.controlKind)
|
|
}
|
|
#endif
|
|
}
|
|
|
|
func writeMessage(_ message: String) {
|
|
if let commandServer {
|
|
commandServer.writeMessage(2, message: message)
|
|
}
|
|
}
|
|
|
|
private func startService() async throws {
|
|
guard let configContent = tunnelOptions?["configContent"] as? String else {
|
|
throw ExtensionStartupError("(packet-tunnel) error: missing configContent in tunnel options")
|
|
}
|
|
|
|
let options = LibboxOverrideOptions()
|
|
do {
|
|
try commandServer!.startOrReloadService(configContent, options: options)
|
|
} catch {
|
|
throw ExtensionStartupError("(packet-tunnel) error: start service: \(error.localizedDescription)")
|
|
}
|
|
#if os(macOS)
|
|
if !Variant.useSystemExtension, commandServer!.needWIFIState() {
|
|
locationManager = CLLocationManager()
|
|
locationDelegate = stubLocationDelegate()
|
|
locationManager?.delegate = locationDelegate
|
|
locationManager?.requestLocation()
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#if os(macOS)
|
|
|
|
class stubLocationDelegate: NSObject, CLLocationManagerDelegate {
|
|
func locationManagerDidChangeAuthorization(_: CLLocationManager) {}
|
|
|
|
func locationManager(_: CLLocationManager, didUpdateLocations _: [CLLocation]) {}
|
|
|
|
func locationManager(_: CLLocationManager, didFailWithError _: Error) {}
|
|
}
|
|
|
|
#endif
|
|
|
|
func stopService() {
|
|
do {
|
|
try commandServer?.closeService()
|
|
} catch {
|
|
writeMessage("(packet-tunnel) stop service: \(error.localizedDescription)")
|
|
}
|
|
if let platformInterface {
|
|
platformInterface.reset()
|
|
}
|
|
}
|
|
|
|
func reloadService() async throws {
|
|
writeMessage("(packet-tunnel) reloading service")
|
|
reasserting = true
|
|
defer {
|
|
reasserting = false
|
|
}
|
|
try await startService()
|
|
}
|
|
|
|
override open func stopTunnel(with reason: NEProviderStopReason) async {
|
|
writeMessage("(packet-tunnel) stopping, reason: \(reason)")
|
|
stopService()
|
|
if let server = commandServer {
|
|
try? await Task.sleep(nanoseconds: 100 * NSEC_PER_MSEC)
|
|
server.close()
|
|
commandServer = nil
|
|
}
|
|
#if os(macOS)
|
|
if Variant.useSystemExtension {
|
|
xpcListener?.invalidate()
|
|
xpcListener = nil
|
|
xpcService?.commandServer = nil
|
|
xpcService = nil
|
|
UserServiceEndpointRegistry.shared.clear()
|
|
}
|
|
locationManager = nil
|
|
locationDelegate = nil
|
|
#endif
|
|
#if os(iOS)
|
|
if #available(iOS 18.0, *) {
|
|
ControlCenter.shared.reloadControls(ofKind: ExtensionProfile.controlKind)
|
|
}
|
|
#endif
|
|
}
|
|
|
|
override open func handleAppMessage(_ messageData: Data) async -> Data? {
|
|
do {
|
|
let options = try ExtensionStartOptions.decode(messageData)
|
|
applyStartOptions(options)
|
|
try persistStartOptions(options)
|
|
try await reloadService()
|
|
return nil
|
|
} catch {
|
|
return error.localizedDescription.data(using: .utf8)
|
|
}
|
|
}
|
|
|
|
override open func sleep() async {
|
|
if let commandServer {
|
|
commandServer.pause()
|
|
}
|
|
}
|
|
|
|
override open func wake() {
|
|
if let commandServer {
|
|
commandServer.wake()
|
|
}
|
|
}
|
|
}
|