Files
sing-box-for-apple/Library/Network/CommandXPC.swift
T

216 lines
8.3 KiB
Swift

#if os(macOS)
import Foundation
import Libbox
import os
private let logger = Logger(category: "CommandXPC")
@objc public protocol CommandXPCProtocol {
func connectToCommandServer(reply: @escaping (FileHandle?, NSError?) -> Void)
func registerUserServiceEndpoint(_ endpoint: NSXPCListenerEndpoint?, reply: @escaping (NSError?) -> Void)
func extensionRequirements(reply: @escaping (Bool, Bool, NSError?) -> Void)
}
class CommandXPCService: NSObject, NSXPCListenerDelegate {
let socketPath: String
var commandServer: LibboxCommandServer?
private let serviceReadyLock = NSLock()
private var _serviceReady = false
private var serviceReadyContinuations: [CheckedContinuation<Void, Never>] = []
init(socketPath: String) {
self.socketPath = socketPath
}
func waitForServiceReady() async {
serviceReadyLock.lock()
if _serviceReady {
serviceReadyLock.unlock()
return
}
await withCheckedContinuation { continuation in
serviceReadyContinuations.append(continuation)
serviceReadyLock.unlock()
}
}
func markServiceReady() {
serviceReadyLock.lock()
_serviceReady = true
let continuations = serviceReadyContinuations
serviceReadyContinuations.removeAll()
serviceReadyLock.unlock()
for continuation in continuations {
continuation.resume()
}
}
func markServiceNotReady() {
serviceReadyLock.lock()
_serviceReady = false
serviceReadyLock.unlock()
}
func listener(_: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
let allowedBundleIDs = [AppConfiguration.packageName + ".standalone"]
guard XPCConnectionValidator.validateConnection(
newConnection,
teamID: AppConfiguration.teamID,
allowedBundleIDs: allowedBundleIDs
) else {
let info = XPCConnectionValidator.getConnectionInfo(newConnection)
logger.warning("Rejected XPC connection: pid=\(info.pid), bundleID=\(info.bundleID ?? "unknown"), teamID=\(info.teamID ?? "unknown")")
return false
}
let exportedInterface = NSXPCInterface(with: CommandXPCProtocol.self)
CommandXPC.configureInterface(exportedInterface)
newConnection.exportedInterface = exportedInterface
newConnection.exportedObject = CommandXPCHandler(service: self)
newConnection.resume()
return true
}
}
private class CommandXPCHandler: NSObject, CommandXPCProtocol {
private let service: CommandXPCService
init(service: CommandXPCService) {
self.service = service
}
func connectToCommandServer(reply: @escaping (FileHandle?, NSError?) -> Void) {
do {
let handle = try connectToUnixSocket(path: service.socketPath)
reply(handle, nil)
} catch {
reply(nil, error as NSError)
}
}
func registerUserServiceEndpoint(_ endpoint: NSXPCListenerEndpoint?, reply: @escaping (NSError?) -> Void) {
if let endpoint {
UserServiceEndpointRegistry.shared.update(endpoint)
} else {
UserServiceEndpointRegistry.shared.clear()
}
reply(nil)
}
func extensionRequirements(reply: @escaping (Bool, Bool, NSError?) -> Void) {
Task {
await service.waitForServiceReady()
guard let commandServer = service.commandServer else {
reply(false, false, NSError(domain: "CommandXPC", code: -1, userInfo: [
NSLocalizedDescriptionKey: "Command server not available",
]))
return
}
let needWIFI = commandServer.needWIFIState()
let needProcess = commandServer.needFindProcess()
reply(needWIFI, needProcess, nil)
}
}
private func connectToUnixSocket(path: String) throws -> FileHandle {
let fd = socket(AF_UNIX, SOCK_STREAM, 0)
guard fd >= 0 else {
throw NSError(domain: "CommandXPC", code: Int(errno), userInfo: [
NSLocalizedDescriptionKey: "Failed to create socket: \(String(cString: strerror(errno)))",
])
}
var addr = sockaddr_un()
addr.sun_family = sa_family_t(AF_UNIX)
let pathSize = MemoryLayout.size(ofValue: addr.sun_path)
withUnsafeMutableBytes(of: &addr.sun_path) { buffer in
_ = path.withCString { cString in
strncpy(buffer.baseAddress!.assumingMemoryBound(to: CChar.self), cString, pathSize - 1)
}
}
let connectResult = withUnsafePointer(to: &addr) { ptr in
ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddrPtr in
connect(fd, sockaddrPtr, socklen_t(MemoryLayout<sockaddr_un>.size))
}
}
guard connectResult >= 0 else {
close(fd)
throw NSError(domain: "CommandXPC", code: Int(errno), userInfo: [
NSLocalizedDescriptionKey: "Failed to connect to \(path): \(String(cString: strerror(errno)))",
])
}
return FileHandle(fileDescriptor: fd, closeOnDealloc: false)
}
}
public class CommandXPCDialer: NSObject, LibboxXPCDialerProtocol {
public static let shared = CommandXPCDialer()
public func dialXPC(_ ret0_: UnsafeMutablePointer<Int32>?) throws {
let semaphore = DispatchSemaphore(value: 0)
var result: Int32 = -1
var resultError: Error?
let machServiceName = AppConfiguration.appGroupID + ".system"
let connection = NSXPCConnection(machServiceName: machServiceName)
let remoteInterface = NSXPCInterface(with: CommandXPCProtocol.self)
CommandXPC.configureInterface(remoteInterface)
connection.remoteObjectInterface = remoteInterface
connection.resume()
let proxy = connection.remoteObjectProxyWithErrorHandler { error in
logger.error("XPC proxy error: \(error.localizedDescription)")
resultError = error
semaphore.signal()
} as! CommandXPCProtocol
proxy.connectToCommandServer { handle, error in
if let error {
logger.error("connectToCommandServer error: \(error.localizedDescription)")
resultError = error
} else if let handle {
result = dup(handle.fileDescriptor)
}
semaphore.signal()
}
semaphore.wait()
connection.invalidate()
if let error = resultError {
throw error
}
if result < 0 {
logger.error("dialXPC failed: No file handle returned")
throw NSError(domain: "CommandXPCDialer", code: -1, userInfo: [
NSLocalizedDescriptionKey: "No file handle returned",
])
}
ret0_?.pointee = result
}
}
public enum CommandXPC {
public static func configureInterface(_ interface: NSXPCInterface) {
let fileHandleClasses = NSSet(array: [FileHandle.self]) as! Set<AnyHashable>
interface.setClasses(
fileHandleClasses,
for: #selector(CommandXPCProtocol.connectToCommandServer(reply:)),
argumentIndex: 0,
ofReply: true
)
let endpointClasses = NSSet(array: [NSXPCListenerEndpoint.self]) as! Set<AnyHashable>
interface.setClasses(
endpointClasses,
for: #selector(CommandXPCProtocol.registerUserServiceEndpoint(_:reply:)),
argumentIndex: 0,
ofReply: false
)
}
}
#endif