Prepare system extension support
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import Foundation
|
||||
import Libbox
|
||||
|
||||
extension LibboxStringIteratorProtocol {
|
||||
func toArray() -> [String] {
|
||||
var array: [String] = []
|
||||
while hasNext() {
|
||||
array.append(next())
|
||||
}
|
||||
return array
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import Foundation
|
||||
import Libbox
|
||||
import NetworkExtension
|
||||
|
||||
func runBlocking<T>(_ body: @escaping () async throws -> T) throws -> T {
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
let box = resultBox<T>()
|
||||
Task {
|
||||
do {
|
||||
let value = try await body()
|
||||
box.result = .success(value)
|
||||
} catch {
|
||||
box.result = .failure(error)
|
||||
}
|
||||
semaphore.signal()
|
||||
}
|
||||
semaphore.wait()
|
||||
return try box.result.get()
|
||||
}
|
||||
|
||||
private class resultBox<T> {
|
||||
var result: Result<T, Error>!
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import Foundation
|
||||
import Libbox
|
||||
import NetworkExtension
|
||||
|
||||
public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol {
|
||||
private let tunnel: NEPacketTunnelProvider
|
||||
private let commandServer: LibboxCommandServer
|
||||
|
||||
init(_ tunnel: NEPacketTunnelProvider, _ logServer: LibboxCommandServer) {
|
||||
self.tunnel = tunnel
|
||||
commandServer = logServer
|
||||
}
|
||||
|
||||
public func openTun(_ options: LibboxTunOptionsProtocol?, ret0_: UnsafeMutablePointer<Int32>?) throws {
|
||||
guard let options else {
|
||||
throw NSError(domain: "nil options", code: 0)
|
||||
}
|
||||
guard let ret0_ else {
|
||||
throw NSError(domain: "nil return pointer", code: 0)
|
||||
}
|
||||
|
||||
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
|
||||
if options.getAutoRoute() {
|
||||
settings.mtu = NSNumber(value: options.getMTU())
|
||||
|
||||
var error: NSError?
|
||||
let dnsServer = options.getDNSServerAddress(&error)
|
||||
if let error {
|
||||
throw error
|
||||
}
|
||||
settings.dnsSettings = NEDNSSettings(servers: [dnsServer])
|
||||
|
||||
var ipv4Address: [String] = []
|
||||
var ipv4Mask: [String] = []
|
||||
let ipv4AddressIterator = options.getInet4Address()!
|
||||
while ipv4AddressIterator.hasNext() {
|
||||
let ipv4Prefix = ipv4AddressIterator.next()!
|
||||
ipv4Address.append(ipv4Prefix.address)
|
||||
ipv4Mask.append(ipv4Prefix.mask())
|
||||
}
|
||||
let ipv4Settings = NEIPv4Settings(addresses: ipv4Address, subnetMasks: ipv4Mask)
|
||||
var ipv4Routes: [NEIPv4Route] = []
|
||||
let inet4RouteAddressIterator = options.getInet4RouteAddress()!
|
||||
if inet4RouteAddressIterator.hasNext() {
|
||||
while inet4RouteAddressIterator.hasNext() {
|
||||
let ipv4RoutePrefix = inet4RouteAddressIterator.next()!
|
||||
ipv4Routes.append(NEIPv4Route(destinationAddress: ipv4RoutePrefix.address, subnetMask: ipv4RoutePrefix.mask()))
|
||||
}
|
||||
} else {
|
||||
ipv4Routes.append(NEIPv4Route.default())
|
||||
}
|
||||
for (index, address) in ipv4Address.enumerated() {
|
||||
ipv4Routes.append(NEIPv4Route(destinationAddress: address, subnetMask: ipv4Mask[index]))
|
||||
}
|
||||
ipv4Settings.includedRoutes = ipv4Routes
|
||||
settings.ipv4Settings = ipv4Settings
|
||||
|
||||
var ipv6Address: [String] = []
|
||||
var ipv6Prefixes: [NSNumber] = []
|
||||
let ipv6AddressIterator = options.getInet6Address()!
|
||||
while ipv6AddressIterator.hasNext() {
|
||||
let ipv6Prefix = ipv6AddressIterator.next()!
|
||||
ipv6Address.append(ipv6Prefix.address)
|
||||
ipv6Prefixes.append(NSNumber(value: ipv6Prefix.prefix))
|
||||
}
|
||||
let ipv6Settings = NEIPv6Settings(addresses: ipv6Address, networkPrefixLengths: ipv6Prefixes)
|
||||
var ipv6Routes: [NEIPv6Route] = []
|
||||
let inet6RouteAddressIterator = options.getInet6RouteAddress()!
|
||||
if inet6RouteAddressIterator.hasNext() {
|
||||
while inet6RouteAddressIterator.hasNext() {
|
||||
let ipv6RoutePrefix = inet4RouteAddressIterator.next()!
|
||||
ipv6Routes.append(NEIPv6Route(destinationAddress: ipv6RoutePrefix.description, networkPrefixLength: NSNumber(value: ipv6RoutePrefix.prefix)))
|
||||
}
|
||||
} else {
|
||||
ipv6Routes.append(NEIPv6Route.default())
|
||||
}
|
||||
ipv6Settings.includedRoutes = ipv6Routes
|
||||
settings.ipv6Settings = ipv6Settings
|
||||
}
|
||||
|
||||
if options.isHTTPProxyEnabled() {
|
||||
let proxySettings = NEProxySettings()
|
||||
let proxyServer = NEProxyServer(address: options.getHTTPProxyServer(), port: Int(options.getHTTPProxyServerPort()))
|
||||
proxySettings.httpEnabled = true
|
||||
proxySettings.httpServer = proxyServer
|
||||
proxySettings.httpsEnabled = true
|
||||
proxySettings.httpsServer = proxyServer
|
||||
settings.proxySettings = proxySettings
|
||||
}
|
||||
|
||||
try runBlocking { [self] in
|
||||
try await tunnel.setTunnelNetworkSettings(settings)
|
||||
}
|
||||
|
||||
if let tunFd = tunnel.packetFlow.value(forKeyPath: "socket.fileDescriptor") as? Int32 {
|
||||
ret0_.pointee = tunFd
|
||||
return
|
||||
}
|
||||
|
||||
let tunFdFromLoop = LibboxGetTunnelFileDescriptor()
|
||||
if tunFdFromLoop != -1 {
|
||||
ret0_.pointee = tunFdFromLoop
|
||||
} else {
|
||||
throw NSError(domain: "missing file descriptor", code: 0)
|
||||
}
|
||||
}
|
||||
|
||||
public func usePlatformAutoDetectControl() -> Bool {
|
||||
true
|
||||
}
|
||||
|
||||
public func autoDetectControl(_: Int32) throws {}
|
||||
|
||||
public func findConnectionOwner(_: Int32, sourceAddress _: String?, sourcePort _: Int32, destinationAddress _: String?, destinationPort _: Int32, ret0_ _: UnsafeMutablePointer<Int32>?) throws {
|
||||
throw NSError(domain: "not implemented", code: 0)
|
||||
}
|
||||
|
||||
public func packageName(byUid _: Int32, error _: NSErrorPointer) -> String {
|
||||
""
|
||||
}
|
||||
|
||||
public func uid(byPackageName _: String?, ret0_ _: UnsafeMutablePointer<Int32>?) throws {
|
||||
throw NSError(domain: "not implemented", code: 0)
|
||||
}
|
||||
|
||||
public func useProcFS() -> Bool {
|
||||
false
|
||||
}
|
||||
|
||||
public func writeLog(_ message: String?) {
|
||||
guard let message else {
|
||||
return
|
||||
}
|
||||
commandServer.writeMessage(message)
|
||||
}
|
||||
|
||||
public func usePlatformDefaultInterfaceMonitor() -> Bool {
|
||||
false
|
||||
}
|
||||
|
||||
public func startDefaultInterfaceMonitor(_: LibboxInterfaceUpdateListenerProtocol?) throws {}
|
||||
|
||||
public func closeDefaultInterfaceMonitor(_: LibboxInterfaceUpdateListenerProtocol?) throws {}
|
||||
|
||||
public func useGetter() -> Bool {
|
||||
false
|
||||
}
|
||||
|
||||
public func getInterfaces() throws -> LibboxNetworkInterfaceIteratorProtocol {
|
||||
throw NSError(domain: "not implemented", code: 0)
|
||||
}
|
||||
|
||||
public func underNetworkExtension() -> Bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,14 @@ public class ExtensionProfile: ObservableObject {
|
||||
public func start() async throws {
|
||||
manager.isEnabled = true
|
||||
try await manager.saveToPreferences()
|
||||
#if os(macOS)
|
||||
if Variant.useSystemExtension {
|
||||
try manager.connection.startVPNTunnel(options: [
|
||||
"username": NSString(string: NSUserName()),
|
||||
])
|
||||
return
|
||||
}
|
||||
#endif
|
||||
try manager.connection.startVPNTunnel()
|
||||
}
|
||||
|
||||
@@ -61,7 +69,11 @@ public class ExtensionProfile: ObservableObject {
|
||||
let manager = NETunnelProviderManager()
|
||||
manager.localizedDescription = "utun interface"
|
||||
let tunnelProtocol = NETunnelProviderProtocol()
|
||||
tunnelProtocol.providerBundleIdentifier = "\(FilePath.packageName).extension"
|
||||
if Variant.useSystemExtension {
|
||||
tunnelProtocol.providerBundleIdentifier = "\(FilePath.packageName).system"
|
||||
} else {
|
||||
tunnelProtocol.providerBundleIdentifier = "\(FilePath.packageName).extension"
|
||||
}
|
||||
tunnelProtocol.serverAddress = "sing-box"
|
||||
manager.protocolConfiguration = tunnelProtocol
|
||||
manager.isEnabled = true
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
import Foundation
|
||||
import Libbox
|
||||
import NetworkExtension
|
||||
|
||||
open class ExtensionProvider: NEPacketTunnelProvider {
|
||||
public var username: String? = nil
|
||||
private var commandServer: LibboxCommandServer!
|
||||
private var boxService: LibboxBoxService!
|
||||
|
||||
override open func startTunnel(options _: [String: NSObject]?) async throws {
|
||||
NSLog("Here I am")
|
||||
|
||||
do {
|
||||
try FileManager.default.createDirectory(at: FilePath.workingDirectory, withIntermediateDirectories: true)
|
||||
} catch {
|
||||
writeFatalError("(packet-tunnel) error: create working directory: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
|
||||
if let username {
|
||||
var error: NSError?
|
||||
LibboxSetupWithUsername(FilePath.workingDirectory.relativePath, FilePath.cacheDirectory.relativePath, username, &error)
|
||||
if let error {
|
||||
writeFatalError("(packet-tunnel) error: setup service: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
LibboxSetup(FilePath.workingDirectory.relativePath, FilePath.cacheDirectory.relativePath)
|
||||
}
|
||||
|
||||
var error: NSError?
|
||||
LibboxRedirectStderr(FilePath.cacheDirectory.appendingPathComponent("stderr.log").relativePath, &error)
|
||||
if let error {
|
||||
writeError("(packet-tunnel) redirect stderr error: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
LibboxSetMemoryLimit(!SharedPreferences.disableMemoryLimit)
|
||||
|
||||
commandServer = LibboxNewCommandServer(FilePath.sharedDirectory.relativePath, serverInterface(self), Int32(SharedPreferences.maxLogLines))
|
||||
do {
|
||||
try commandServer.start()
|
||||
} catch {
|
||||
writeFatalError("(packet-tunnel): log server start error: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
writeMessage("(packet-tunnel) log server started")
|
||||
|
||||
startService()
|
||||
}
|
||||
|
||||
private func writeMessage(_ message: String) {
|
||||
if let commandServer {
|
||||
commandServer.writeMessage(message)
|
||||
} else {
|
||||
NSLog(message)
|
||||
}
|
||||
}
|
||||
|
||||
private func writeError(_ message: String) {
|
||||
writeMessage(message)
|
||||
#if os(iOS)
|
||||
ServiceNotification.postServiceNotification(title: "Service Error", message: message)
|
||||
#else
|
||||
if Variant.useSystemExtension {
|
||||
NSLog(message)
|
||||
} else {
|
||||
displayMessage(message) { _ in
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public func writeFatalError(_ message: String) {
|
||||
writeError(message)
|
||||
cancelTunnelWithError(NSError(domain: message, code: 0))
|
||||
}
|
||||
|
||||
private func startService() {
|
||||
let profile: Profile?
|
||||
do {
|
||||
profile = try ProfileManager.get(Int64(SharedPreferences.selectedProfileID))
|
||||
} catch {
|
||||
writeFatalError("(packet-tunnel) error: missing default profile: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
guard let profile else {
|
||||
writeFatalError("(packet-tunnel) error: missing default profile")
|
||||
return
|
||||
}
|
||||
let configContent: String
|
||||
do {
|
||||
configContent = try profile.read()
|
||||
} catch {
|
||||
writeFatalError("(packet-tunnel) error: read config file: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
var error: NSError?
|
||||
let service = LibboxNewService(configContent, ExtensionPlatformInterface(self, commandServer), &error)
|
||||
if let error {
|
||||
writeError("(packet-tunnel) error: create service: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
guard let service else {
|
||||
return
|
||||
}
|
||||
do {
|
||||
try service.start()
|
||||
} catch {
|
||||
writeError("(packet-tunnel) error: start service: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
boxService = service
|
||||
commandServer.setService(service)
|
||||
#if os(macOS)
|
||||
Task.detached {
|
||||
SharedPreferences.startedByUser = true
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private func stopService() {
|
||||
if let service = boxService {
|
||||
do {
|
||||
try service.close()
|
||||
} catch {
|
||||
writeError("(packet-tunnel) error: stop service: \(error.localizedDescription)")
|
||||
}
|
||||
boxService = nil
|
||||
commandServer.setService(nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func reloadService() {
|
||||
writeMessage("(packet-tunnel) reloading service")
|
||||
reasserting = true
|
||||
defer {
|
||||
reasserting = false
|
||||
}
|
||||
stopService()
|
||||
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)
|
||||
try? server.close()
|
||||
commandServer = nil
|
||||
}
|
||||
#if os(macOS)
|
||||
if reason == .userInitiated {
|
||||
SharedPreferences.startedByUser = reason == .userInitiated
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
override open func handleAppMessage(_ messageData: Data) async -> Data? {
|
||||
messageData
|
||||
}
|
||||
|
||||
override open func sleep() async {}
|
||||
|
||||
override open func wake() {}
|
||||
|
||||
private class serverInterface: NSObject, LibboxCommandServerHandlerProtocol {
|
||||
unowned let tunnel: ExtensionProvider
|
||||
|
||||
init(_ tunnel: ExtensionProvider) {
|
||||
self.tunnel = tunnel
|
||||
super.init()
|
||||
}
|
||||
|
||||
func serviceReload() throws {
|
||||
tunnel.reloadService()
|
||||
}
|
||||
|
||||
func serviceStop() throws {
|
||||
tunnel.stopService()
|
||||
tunnel.writeMessage("(packet-tunnel) debug: service stopped")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
#if os(macOS)
|
||||
import Foundation
|
||||
import SystemExtensions
|
||||
|
||||
public class SystemExtension: NSObject, OSSystemExtensionRequestDelegate {
|
||||
private let forceUpdate: Bool
|
||||
private let semaphore = DispatchSemaphore(value: 0)
|
||||
private var result: OSSystemExtensionRequest.Result?
|
||||
private var properties: [OSSystemExtensionProperties]?
|
||||
private var error: Error?
|
||||
|
||||
private init(forceUpdate: Bool = false) {
|
||||
self.forceUpdate = forceUpdate
|
||||
}
|
||||
|
||||
public func request(_: OSSystemExtensionRequest, actionForReplacingExtension existing: OSSystemExtensionProperties, withExtension ext: OSSystemExtensionProperties) -> OSSystemExtensionRequest.ReplacementAction {
|
||||
if forceUpdate {
|
||||
return .replace
|
||||
}
|
||||
if existing.bundleIdentifier == ext.bundleIdentifier,
|
||||
existing.bundleVersion == ext.bundleVersion
|
||||
{
|
||||
return .cancel
|
||||
} else {
|
||||
return .replace
|
||||
}
|
||||
}
|
||||
|
||||
public func requestNeedsUserApproval(_: OSSystemExtensionRequest) {
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
public func request(_: OSSystemExtensionRequest, didFinishWithResult result: OSSystemExtensionRequest.Result) {
|
||||
self.result = result
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
public func request(_: OSSystemExtensionRequest, didFailWithError error: Error) {
|
||||
self.error = error
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
public func request(_: OSSystemExtensionRequest, foundProperties properties: [OSSystemExtensionProperties]) {
|
||||
self.properties = properties
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
public func submitAndWait() throws -> OSSystemExtensionRequest.Result? {
|
||||
let request = OSSystemExtensionRequest.activationRequest(forExtensionWithIdentifier: FilePath.packageName + ".system", queue: .main)
|
||||
request.delegate = self
|
||||
OSSystemExtensionManager.shared.submitRequest(request)
|
||||
semaphore.wait()
|
||||
if let error {
|
||||
throw error
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func getProperties() throws -> [OSSystemExtensionProperties] {
|
||||
let request = OSSystemExtensionRequest.propertiesRequest(forExtensionWithIdentifier: FilePath.packageName + ".system", queue: .main)
|
||||
request.delegate = self
|
||||
OSSystemExtensionManager.shared.submitRequest(request)
|
||||
semaphore.wait()
|
||||
if let error {
|
||||
throw error
|
||||
}
|
||||
return properties!
|
||||
}
|
||||
|
||||
public static func isInstalled() async -> Bool {
|
||||
await (try? Task.detached {
|
||||
do {
|
||||
let propList = try SystemExtension().getProperties()
|
||||
if propList.isEmpty {
|
||||
return false
|
||||
}
|
||||
for extensionProp in propList {
|
||||
if !extensionProp.isAwaitingUserApproval, !extensionProp.isUninstalling {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
NSLog(error.localizedDescription)
|
||||
}
|
||||
return false
|
||||
}.result.get()) == true
|
||||
}
|
||||
|
||||
public static func install(forceUpdate: Bool = false) async throws -> OSSystemExtensionRequest.Result? {
|
||||
try await Task.detached {
|
||||
try SystemExtension(forceUpdate: forceUpdate).submitAndWait()
|
||||
}.result.get()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user