Files
sing-box-for-apple/Library/Network/ExtensionProvider.swift
T
2026-04-23 08:12:53 +08:00

332 lines
12 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 lazy var platformInterface = ExtensionPlatformInterface(self)
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
overridePreferences = OverridePreferences(
includeAllNetworks: (options["includeAllNetworks"] as? NSNumber)?.boolValue ?? false,
systemProxyEnabled: (options["systemProxyEnabled"] as? NSNumber)?.boolValue ?? true,
excludeDefaultRoute: (options["excludeDefaultRoute"] as? NSNumber)?.boolValue ?? false,
autoRouteUseSubRangesByDefault: (options["autoRouteUseSubRangesByDefault"] as? NSNumber)?.boolValue ?? false,
excludeAPNsRoute: (options["excludeAPNsRoute"] as? NSNumber)?.boolValue ?? false
)
}
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)
}
private func loadPersistedStartOptions() throws -> [String: NSObject]? {
guard let startOptionsURL, FileManager.default.fileExists(atPath: startOptionsURL.path) else {
return nil
}
let data = try Data(contentsOf: startOptionsURL)
return try ExtensionStartOptions.decode(data)
}
private func resolveStartOptions(_ startOptions: [String: NSObject]?) throws -> [String: NSObject] {
if let startOptions, startOptions["configContent"] as? String != nil {
return startOptions
}
let persistedOptions: [String: NSObject]?
do {
persistedOptions = try loadPersistedStartOptions()
} catch {
throw ExtensionStartupError("(packet-tunnel) error: load start options: \(error.localizedDescription)")
}
if let persistedOptions {
if let startOptions {
return persistedOptions.merging(startOptions) { _, new in new }
}
return persistedOptions
}
throw ExtensionStartupError("(packet-tunnel) error: missing start options")
}
#if os(macOS)
private var xpcListener: NSXPCListener!
private var xpcService: CommandXPCService!
private var locationManager: CLLocationManager?
private var locationDelegate: stubLocationDelegate?
#endif
override public init() {
LibboxPrepareCrashSignalHandlers()
#if os(macOS)
if Variant.useSystemExtension {
NativeCrashReporter.installForCurrentProcess(
basePath: FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("NativeCrash")
)
} else {
NativeCrashReporter.installForCurrentProcess()
}
#else
NativeCrashReporter.installForCurrentProcess()
#endif
LibboxReinstallCrashSignalHandlers()
super.init()
}
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)
#if os(macOS)
if Variant.useSystemExtension {
let socketPath = basePath + "/command.sock"
let machServiceName = AppConfiguration.appGroupID + ".system"
xpcService = CommandXPCService(socketPath: socketPath)
xpcListener = NSXPCListener(machServiceName: machServiceName)
xpcListener.delegate = xpcService
}
#endif
let effectiveOptions = try resolveStartOptions(startOptions)
if effectiveOptions["configContent"] == nil {
throw ExtensionStartupError("(packet-tunnel) error: missing configContent in tunnel options")
}
do {
try persistStartOptions(effectiveOptions)
} catch {
throw ExtensionStartupError("(packet-tunnel) error: persist start options: \(error.localizedDescription)")
}
applyStartOptions(effectiveOptions)
let options = LibboxSetupOptions()
options.basePath = basePath
options.workingPath = workingPath
options.tempPath = tempPath
options.logMaxLines = 3000
options.debug = SharedPreferences.inDebug
options.crashReportSource = "NetworkExtension"
#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
#if os(macOS)
options.oomKillerEnabled = (effectiveOptions["oomKillerEnabled"] as? NSNumber)?.boolValue ?? false
let oomMemoryLimitMB = (effectiveOptions["oomMemoryLimitMB"] as? NSNumber)?.int64Value ?? 0
options.oomMemoryLimit = oomMemoryLimitMB * 1024 * 1024
options.oomKillerDisabled = !((effectiveOptions["oomKillerKillConnections"] as? NSNumber)?.boolValue ?? false)
#else
options.oomKillerEnabled = true
#endif
var setupError: NSError?
LibboxSetup(options, &setupError)
if let setupError {
throw ExtensionStartupError("(packet-tunnel) error: setup service: \(setupError.localizedDescription)")
}
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 {
xpcListener.resume()
Self.logger.info("set Command Server")
xpcService.commandServer = commandServer
}
#endif
do {
try await startService()
} catch {
#if os(macOS)
if Variant.useSystemExtension {
xpcService.markServiceNotReady(error)
}
#endif
throw error
}
writeMessage("(packet-tunnel): Here I stand")
#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)")
}
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 {
xpcService.markServiceNotReady(NSError(domain: "CommandXPC", code: -1, userInfo: [
NSLocalizedDescriptionKey: "Command server stopped",
]))
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()
}
}
}