Prepare system extension support
@@ -1,12 +1,60 @@
|
|||||||
|
import Library
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
public struct DashboardView: View {
|
public struct DashboardView: View {
|
||||||
@Environment(\.extensionProfile) private var extensionProfile
|
#if os(macOS)
|
||||||
|
@Environment(\.controlActiveState) private var controlActiveState
|
||||||
|
@State private var isLoading = true
|
||||||
|
@State private var systemExtensionInstalled = true
|
||||||
|
#endif
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
viewBuilder {
|
viewBuilder {
|
||||||
|
#if os(macOS)
|
||||||
|
if Variant.useSystemExtension {
|
||||||
|
viewBuilder {
|
||||||
|
if !systemExtensionInstalled {
|
||||||
|
FormView {
|
||||||
|
InstallSystemExtensionButton(reload)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DashboardView0()
|
||||||
|
}
|
||||||
|
}.onAppear(perform: reload)
|
||||||
|
} else {
|
||||||
|
DashboardView0()
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
DashboardView0()
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
.onChange(of: controlActiveState, perform: { newValue in
|
||||||
|
if newValue != .inactive {
|
||||||
|
if Variant.useSystemExtension {
|
||||||
|
if !isLoading {
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
#endif
|
||||||
|
.navigationTitle("Dashboard")
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
private func reload() {
|
||||||
|
Task {
|
||||||
|
systemExtensionInstalled = await SystemExtension.isInstalled()
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct DashboardView0: View {
|
||||||
|
@Environment(\.extensionProfile) private var extensionProfile
|
||||||
|
var body: some View {
|
||||||
if ApplicationLibrary.inPreview {
|
if ApplicationLibrary.inPreview {
|
||||||
ActiveDashboardView()
|
ActiveDashboardView()
|
||||||
} else if let profile = extensionProfile.wrappedValue {
|
} else if let profile = extensionProfile.wrappedValue {
|
||||||
@@ -16,6 +64,6 @@ public struct DashboardView: View {
|
|||||||
InstallProfileButton()
|
InstallProfileButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.navigationTitle("Dashboard")
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#if os(macOS)
|
||||||
|
|
||||||
|
import Library
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct InstallSystemExtensionButton: View {
|
||||||
|
@State private var errorPresented = false
|
||||||
|
@State private var errorMessage = ""
|
||||||
|
private let callback: () -> Void
|
||||||
|
public init(_ callback: @escaping () -> Void) {
|
||||||
|
self.callback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
Button("Install SystemExtension") {
|
||||||
|
Task {
|
||||||
|
await installSystemExtension()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert(isPresented: $errorPresented) {
|
||||||
|
Alert(
|
||||||
|
title: Text("Error"),
|
||||||
|
message: Text(errorMessage),
|
||||||
|
dismissButton: .default(Text("Ok"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func installSystemExtension() async {
|
||||||
|
do {
|
||||||
|
if let result = try await SystemExtension.install() {
|
||||||
|
if result == .willCompleteAfterReboot {
|
||||||
|
errorMessage = "Need reboot"
|
||||||
|
errorPresented = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
} catch {
|
||||||
|
errorMessage = error.localizedDescription
|
||||||
|
errorPresented = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -48,7 +48,7 @@ public struct EditProfileContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.font(Font.system(.caption2, design: .monospaced))
|
.font(Font.system(.caption2, design: .monospaced))
|
||||||
.disableAutocorrection(true)
|
.autocorrectionDisabled()
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.textInputAutocapitalization(.none)
|
.textInputAutocapitalization(.none)
|
||||||
.background(Color(UIColor.secondarySystemGroupedBackground))
|
.background(Color(UIColor.secondarySystemGroupedBackground))
|
||||||
|
|||||||
@@ -74,6 +74,27 @@ public struct SettingView: View {
|
|||||||
SharedPreferences.disableMemoryLimit = newValue
|
SharedPreferences.disableMemoryLimit = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
if Variant.useSystemExtension {
|
||||||
|
HStack {
|
||||||
|
Button("Update System Extension") {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
if let result = try await SystemExtension.install(forceUpdate: true) {
|
||||||
|
if result == .willCompleteAfterReboot {
|
||||||
|
errorMessage = "Need reboot"
|
||||||
|
errorPresented = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
errorMessage = error.localizedDescription
|
||||||
|
errorPresented = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.frame(maxWidth: .infinity, alignment: .trailing)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
Section("Core") {
|
Section("Core") {
|
||||||
FormTextItem("Version", version)
|
FormTextItem("Version", version)
|
||||||
|
|||||||
@@ -1,174 +1,4 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Libbox
|
|
||||||
import Library
|
import Library
|
||||||
import NetworkExtension
|
|
||||||
|
|
||||||
class PacketTunnelProvider: NEPacketTunnelProvider {
|
class PacketTunnelProvider: ExtensionProvider {}
|
||||||
private var commandServer: LibboxCommandServer!
|
|
||||||
private var boxService: LibboxBoxService!
|
|
||||||
|
|
||||||
override func startTunnel(options _: [String: NSObject]?) async throws {
|
|
||||||
NSLog("Here I am")
|
|
||||||
do {
|
|
||||||
try FileManager.default.createDirectory(at: FilePath.cacheDirectory, withIntermediateDirectories: true)
|
|
||||||
} catch {
|
|
||||||
writeFatalError("(packet-tunnel) error: create cache directory: \(error.localizedDescription)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
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")
|
|
||||||
|
|
||||||
do {
|
|
||||||
try FileManager.default.createDirectory(at: FilePath.workingDirectory, withIntermediateDirectories: true)
|
|
||||||
} catch {
|
|
||||||
writeFatalError("(packet-tunnel) error: create working directory: \(error.localizedDescription)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
LibboxSetup(FilePath.workingDirectory.relativePath, FilePath.cacheDirectory.relativePath, -1, -1)
|
|
||||||
|
|
||||||
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
|
|
||||||
displayMessage(message) { _ in
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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 func stopTunnel(with reason: NEProviderStopReason) async {
|
|
||||||
writeMessage("(packet-tunnel) stopping, reason: \(reason)")
|
|
||||||
stopService()
|
|
||||||
if let server = commandServer {
|
|
||||||
try? server.close()
|
|
||||||
commandServer = nil
|
|
||||||
}
|
|
||||||
#if os(macOS)
|
|
||||||
if reason == .userInitiated {
|
|
||||||
SharedPreferences.startedByUser = reason == .userInitiated
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
override func handleAppMessage(_ messageData: Data) async -> Data? {
|
|
||||||
messageData
|
|
||||||
}
|
|
||||||
|
|
||||||
override func sleep() async {}
|
|
||||||
|
|
||||||
override func wake() {}
|
|
||||||
|
|
||||||
private class serverInterface: NSObject, LibboxCommandServerHandlerProtocol {
|
|
||||||
unowned let tunnel: PacketTunnelProvider
|
|
||||||
|
|
||||||
init(_ tunnel: PacketTunnelProvider) {
|
|
||||||
self.tunnel = tunnel
|
|
||||||
super.init()
|
|
||||||
}
|
|
||||||
|
|
||||||
func serviceReload() throws {
|
|
||||||
tunnel.reloadService()
|
|
||||||
}
|
|
||||||
|
|
||||||
func serviceStop() throws {
|
|
||||||
tunnel.stopService()
|
|
||||||
tunnel.writeMessage("(packet-tunnel) debug: service stopped")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class Database {
|
|||||||
if let writer {
|
if let writer {
|
||||||
return writer
|
return writer
|
||||||
}
|
}
|
||||||
|
try FileManager.default.createDirectory(at: FilePath.sharedDirectory, withIntermediateDirectories: true)
|
||||||
let database = try DatabasePool(path: FilePath.sharedDirectory.appendingPathComponent("settings.db").relativePath)
|
let database = try DatabasePool(path: FilePath.sharedDirectory.appendingPathComponent("settings.db").relativePath)
|
||||||
var migrator = DatabaseMigrator().disablingDeferredForeignKeyChecks()
|
var migrator = DatabaseMigrator().disablingDeferredForeignKeyChecks()
|
||||||
migrator.eraseDatabaseOnSchemaChange = true
|
migrator.eraseDatabaseOnSchemaChange = true
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Foundation
|
|||||||
import Libbox
|
import Libbox
|
||||||
import NetworkExtension
|
import NetworkExtension
|
||||||
|
|
||||||
class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol {
|
public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol {
|
||||||
private let tunnel: NEPacketTunnelProvider
|
private let tunnel: NEPacketTunnelProvider
|
||||||
private let commandServer: LibboxCommandServer
|
private let commandServer: LibboxCommandServer
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol {
|
|||||||
commandServer = logServer
|
commandServer = logServer
|
||||||
}
|
}
|
||||||
|
|
||||||
func openTun(_ options: LibboxTunOptionsProtocol?, ret0_: UnsafeMutablePointer<Int32>?) throws {
|
public func openTun(_ options: LibboxTunOptionsProtocol?, ret0_: UnsafeMutablePointer<Int32>?) throws {
|
||||||
guard let options else {
|
guard let options else {
|
||||||
throw NSError(domain: "nil options", code: 0)
|
throw NSError(domain: "nil options", code: 0)
|
||||||
}
|
}
|
||||||
@@ -105,52 +105,52 @@ class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func usePlatformAutoDetectControl() -> Bool {
|
public func usePlatformAutoDetectControl() -> Bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
func autoDetectControl(_: Int32) throws {}
|
public func autoDetectControl(_: Int32) throws {}
|
||||||
|
|
||||||
func findConnectionOwner(_: Int32, sourceAddress _: String?, sourcePort _: Int32, destinationAddress _: String?, destinationPort _: Int32, ret0_ _: UnsafeMutablePointer<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)
|
throw NSError(domain: "not implemented", code: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func packageName(byUid _: Int32, error _: NSErrorPointer) -> String {
|
public func packageName(byUid _: Int32, error _: NSErrorPointer) -> String {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
func uid(byPackageName _: String?, ret0_ _: UnsafeMutablePointer<Int32>?) throws {
|
public func uid(byPackageName _: String?, ret0_ _: UnsafeMutablePointer<Int32>?) throws {
|
||||||
throw NSError(domain: "not implemented", code: 0)
|
throw NSError(domain: "not implemented", code: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func useProcFS() -> Bool {
|
public func useProcFS() -> Bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeLog(_ message: String?) {
|
public func writeLog(_ message: String?) {
|
||||||
guard let message else {
|
guard let message else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
commandServer.writeMessage(message)
|
commandServer.writeMessage(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
func usePlatformDefaultInterfaceMonitor() -> Bool {
|
public func usePlatformDefaultInterfaceMonitor() -> Bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
func startDefaultInterfaceMonitor(_: LibboxInterfaceUpdateListenerProtocol?) throws {}
|
public func startDefaultInterfaceMonitor(_: LibboxInterfaceUpdateListenerProtocol?) throws {}
|
||||||
|
|
||||||
func closeDefaultInterfaceMonitor(_: LibboxInterfaceUpdateListenerProtocol?) throws {}
|
public func closeDefaultInterfaceMonitor(_: LibboxInterfaceUpdateListenerProtocol?) throws {}
|
||||||
|
|
||||||
func useGetter() -> Bool {
|
public func useGetter() -> Bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
func getInterfaces() throws -> LibboxNetworkInterfaceIteratorProtocol {
|
public func getInterfaces() throws -> LibboxNetworkInterfaceIteratorProtocol {
|
||||||
throw NSError(domain: "not implemented", code: 0)
|
throw NSError(domain: "not implemented", code: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func underNetworkExtension() -> Bool {
|
public func underNetworkExtension() -> Bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,14 @@ public class ExtensionProfile: ObservableObject {
|
|||||||
public func start() async throws {
|
public func start() async throws {
|
||||||
manager.isEnabled = true
|
manager.isEnabled = true
|
||||||
try await manager.saveToPreferences()
|
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()
|
try manager.connection.startVPNTunnel()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +69,11 @@ public class ExtensionProfile: ObservableObject {
|
|||||||
let manager = NETunnelProviderManager()
|
let manager = NETunnelProviderManager()
|
||||||
manager.localizedDescription = "utun interface"
|
manager.localizedDescription = "utun interface"
|
||||||
let tunnelProtocol = NETunnelProviderProtocol()
|
let tunnelProtocol = NETunnelProviderProtocol()
|
||||||
|
if Variant.useSystemExtension {
|
||||||
|
tunnelProtocol.providerBundleIdentifier = "\(FilePath.packageName).system"
|
||||||
|
} else {
|
||||||
tunnelProtocol.providerBundleIdentifier = "\(FilePath.packageName).extension"
|
tunnelProtocol.providerBundleIdentifier = "\(FilePath.packageName).extension"
|
||||||
|
}
|
||||||
tunnelProtocol.serverAddress = "sing-box"
|
tunnelProtocol.serverAddress = "sing-box"
|
||||||
manager.protocolConfiguration = tunnelProtocol
|
manager.protocolConfiguration = tunnelProtocol
|
||||||
manager.isEnabled = true
|
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
|
||||||
@@ -12,15 +12,25 @@ public enum FilePath {
|
|||||||
public extension FilePath {
|
public extension FilePath {
|
||||||
static let groupName = "group.\(packageName)"
|
static let groupName = "group.\(packageName)"
|
||||||
|
|
||||||
static let sharedDirectory: URL! = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupName)
|
static var sharedDirectory = defaultSharedDirectory
|
||||||
|
|
||||||
static let cacheDirectory = sharedDirectory
|
private static var defaultSharedDirectory: URL {
|
||||||
|
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: FilePath.groupName)!
|
||||||
|
}
|
||||||
|
|
||||||
|
static var cacheDirectory: URL {
|
||||||
|
sharedDirectory
|
||||||
.appendingPathComponent("Library", isDirectory: true)
|
.appendingPathComponent("Library", isDirectory: true)
|
||||||
.appendingPathComponent("Caches", isDirectory: true)
|
.appendingPathComponent("Caches", isDirectory: true)
|
||||||
|
}
|
||||||
|
|
||||||
static let workingDirectory = cacheDirectory.appendingPathComponent("Working", isDirectory: true)
|
static var workingDirectory: URL {
|
||||||
|
cacheDirectory.appendingPathComponent("Working", isDirectory: true)
|
||||||
|
}
|
||||||
|
|
||||||
static let iCloudDirectory = FileManager.default.url(forUbiquityContainerIdentifier: nil)!.appendingPathComponent("Documents", isDirectory: true)
|
static var iCloudDirectory: URL {
|
||||||
|
FileManager.default.url(forUbiquityContainerIdentifier: nil)!.appendingPathComponent("Documents", isDirectory: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension URL {
|
public extension URL {
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
public enum Variant {
|
||||||
|
#if os(macOS)
|
||||||
|
public static var useSystemExtension = false
|
||||||
|
#else
|
||||||
|
public static let useSystemExtension = false
|
||||||
|
#endif
|
||||||
|
}
|
||||||
@@ -4,8 +4,8 @@ import Foundation
|
|||||||
import Libbox
|
import Libbox
|
||||||
import Library
|
import Library
|
||||||
|
|
||||||
class ApplicationDelegate: NSObject, NSApplicationDelegate {
|
open class ApplicationDelegate: NSObject, NSApplicationDelegate {
|
||||||
func applicationDidFinishLaunching(_: Notification) {
|
public func applicationDidFinishLaunching(_: Notification) {
|
||||||
NSLog("Here I stand")
|
NSLog("Here I stand")
|
||||||
// ServiceNotification.register() // Not work
|
// ServiceNotification.register() // Not work
|
||||||
let event = NSAppleEventManager.shared().currentAppleEvent
|
let event = NSAppleEventManager.shared().currentAppleEvent
|
||||||
@@ -20,6 +20,15 @@ class ApplicationDelegate: NSObject, NSApplicationDelegate {
|
|||||||
}
|
}
|
||||||
Task.detached {
|
Task.detached {
|
||||||
do {
|
do {
|
||||||
|
if Variant.useSystemExtension {
|
||||||
|
if await SystemExtension.isInstalled() {
|
||||||
|
if let result = try await SystemExtension.install() {
|
||||||
|
if result == .willCompleteAfterReboot {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
try await self.postStart(launchedAsLogInItem)
|
try await self.postStart(launchedAsLogInItem)
|
||||||
} catch {
|
} catch {
|
||||||
NSLog("application setup error: \(error.localizedDescription)")
|
NSLog("application setup error: \(error.localizedDescription)")
|
||||||
@@ -38,11 +47,11 @@ class ApplicationDelegate: NSObject, NSApplicationDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool {
|
public func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool {
|
||||||
!SharedPreferences.menuBarExtraInBackground
|
!SharedPreferences.menuBarExtraInBackground
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationShouldHandleReopen(_: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
|
public func applicationShouldHandleReopen(_: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
|
||||||
if !flag, NSApp.activationPolicy() == .accessory {
|
if !flag, NSApp.activationPolicy() == .accessory {
|
||||||
NSApp.setActivationPolicy(.regular)
|
NSApp.setActivationPolicy(.regular)
|
||||||
NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.dock").first?.activate()
|
NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.dock").first?.activate()
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import ApplicationLibrary
|
||||||
|
import Library
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct MacApplication: Scene {
|
||||||
|
@State private var showMenuBarExtra = false
|
||||||
|
@State private var isMenuPresented = false
|
||||||
|
|
||||||
|
public init() {}
|
||||||
|
public var body: some Scene {
|
||||||
|
Window("sing-box", id: "main", content: {
|
||||||
|
MainView()
|
||||||
|
.onAppear {
|
||||||
|
Task.detached {
|
||||||
|
await initialize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.environment(\.showMenuBarExtra, $showMenuBarExtra)
|
||||||
|
})
|
||||||
|
.commands {
|
||||||
|
if showMenuBarExtra {
|
||||||
|
CommandGroup(replacing: .appTermination) {
|
||||||
|
Button("Quit sing-box") {
|
||||||
|
hide(closeApp: true)
|
||||||
|
}
|
||||||
|
.keyboardShortcut("q", modifiers: [.command])
|
||||||
|
}
|
||||||
|
CommandGroup(replacing: .saveItem) {
|
||||||
|
Button("Close") {
|
||||||
|
hide(closeApp: false)
|
||||||
|
}
|
||||||
|
.keyboardShortcut("w", modifiers: [.command])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SidebarCommands()
|
||||||
|
}
|
||||||
|
|
||||||
|
Window("New Profile", id: NewProfileView.windowID) {
|
||||||
|
NewProfileView()
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowGroup("Edit Profile", id: EditProfileWindowView.windowID, for: Int64.self) { profileID in
|
||||||
|
EditProfileWindowView(profileID.wrappedValue)
|
||||||
|
}.commandsRemoved()
|
||||||
|
|
||||||
|
WindowGroup("Edit Content", id: EditProfileContentView.windowID, for: EditProfileContentView.Context.self) { context in
|
||||||
|
EditProfileContentView(context.wrappedValue)
|
||||||
|
}.commandsRemoved()
|
||||||
|
|
||||||
|
Window("Service Log", id: ServiceLogView.windowID) {
|
||||||
|
ServiceLogView()
|
||||||
|
}
|
||||||
|
MenuBarExtra(isInserted: $showMenuBarExtra) {
|
||||||
|
MenuView(isMenuPresented: $isMenuPresented)
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "network.badge.shield.half.filled")
|
||||||
|
}
|
||||||
|
.menuBarExtraStyle(.window)
|
||||||
|
.menuBarExtraAccess(isPresented: $isMenuPresented)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func initialize() {
|
||||||
|
let initialShowMenuBarExtra = SharedPreferences.showMenuBarExtra
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
showMenuBarExtra = initialShowMenuBarExtra
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hide(closeApp: Bool) {
|
||||||
|
Task.detached {
|
||||||
|
if SharedPreferences.menuBarExtraInBackground {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
hide0(closeApp: closeApp)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if closeApp {
|
||||||
|
NSApp.terminate(nil)
|
||||||
|
} else {
|
||||||
|
NSApp.keyWindow?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hide0(closeApp: Bool) {
|
||||||
|
if closeApp || NSApp.keyWindow?.identifier?.rawValue == "main" {
|
||||||
|
let transformState = ProcessApplicationTransformState(kProcessTransformToUIElementApplication)
|
||||||
|
var psn = ProcessSerialNumber(highLongOfPSN: 0, lowLongOfPSN: UInt32(kCurrentProcess))
|
||||||
|
TransformProcessType(&psn, transformState)
|
||||||
|
NSApp.setActivationPolicy(.accessory)
|
||||||
|
}
|
||||||
|
NSApp.keyWindow?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
public class MacLibrary {}
|
||||||
@@ -2,7 +2,7 @@ import ApplicationLibrary
|
|||||||
import Library
|
import Library
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct MainView: View {
|
public struct MainView: View {
|
||||||
@Environment(\.controlActiveState) private var controlActiveState
|
@Environment(\.controlActiveState) private var controlActiveState
|
||||||
|
|
||||||
@State private var selection = NavigationPage.dashboard
|
@State private var selection = NavigationPage.dashboard
|
||||||
@@ -14,7 +14,8 @@ struct MainView: View {
|
|||||||
@State private var serviceNotificationContent = ""
|
@State private var serviceNotificationContent = ""
|
||||||
@State private var serviceNotificationPresented = false
|
@State private var serviceNotificationPresented = false
|
||||||
|
|
||||||
var body: some View {
|
public init() {}
|
||||||
|
public var body: some View {
|
||||||
NavigationSplitView {
|
NavigationSplitView {
|
||||||
VStack {
|
VStack {
|
||||||
SidebarView()
|
SidebarView()
|
||||||
@@ -6,17 +6,21 @@ import MacControlCenterUI
|
|||||||
import MenuBarExtraAccess
|
import MenuBarExtraAccess
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct MenuView: View {
|
public struct MenuView: View {
|
||||||
@Environment(\.openWindow) private var openWindow
|
@Environment(\.openWindow) private var openWindow
|
||||||
|
|
||||||
private static let sliderWidth: CGFloat = 270
|
private static let sliderWidth: CGFloat = 270
|
||||||
|
|
||||||
@Binding var isMenuPresented: Bool
|
@Binding private var isMenuPresented: Bool
|
||||||
|
|
||||||
@State private var isLoading = true
|
@State private var isLoading = true
|
||||||
@State private var profile: ExtensionProfile?
|
@State private var profile: ExtensionProfile?
|
||||||
|
|
||||||
var body: some View {
|
public init(isMenuPresented: Binding<Bool>) {
|
||||||
|
_isMenuPresented = isMenuPresented
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
MacControlCenterMenu(isPresented: $isMenuPresented) {
|
MacControlCenterMenu(isPresented: $isMenuPresented) {
|
||||||
MenuHeader("sing-box") {
|
MenuHeader("sing-box") {
|
||||||
if isLoading {
|
if isLoading {
|
||||||
@@ -2,11 +2,12 @@ import ApplicationLibrary
|
|||||||
import Library
|
import Library
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SidebarView: View {
|
public struct SidebarView: View {
|
||||||
@Environment(\.selection) private var selection
|
@Environment(\.selection) private var selection
|
||||||
@Environment(\.extensionProfile) private var extensionProfile
|
@Environment(\.extensionProfile) private var extensionProfile
|
||||||
|
|
||||||
var body: some View {
|
public init() {}
|
||||||
|
public var body: some View {
|
||||||
viewBuilder {
|
viewBuilder {
|
||||||
if let profile = extensionProfile.wrappedValue {
|
if let profile = extensionProfile.wrappedValue {
|
||||||
SidebarView0().environmentObject(profile)
|
SidebarView0().environmentObject(profile)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import Library
|
||||||
|
import MacLibrary
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct Application: App {
|
||||||
|
@NSApplicationDelegateAdaptor private var appDelegate: ApplicationDelegate
|
||||||
|
|
||||||
|
init() {
|
||||||
|
Variant.useSystemExtension = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some Scene {
|
||||||
|
MacApplication()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "apple-16 1x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "16x16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "apple-16 2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "16x16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "apple-32 1x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "32x32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "apple-32 2x 1.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "32x32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "apple-128 1x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "128x128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "apple-128 2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "128x128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "apple-256 1x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "256x256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "apple-256 2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "256x256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "apple-512 1x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "512x512"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "apple-512 2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 286 KiB |
|
After Width: | Height: | Size: 273 KiB |
|
After Width: | Height: | Size: 273 KiB |
|
After Width: | Height: | Size: 286 KiB |
|
After Width: | Height: | Size: 306 KiB |
|
After Width: | Height: | Size: 272 KiB |
|
After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 306 KiB |
|
After Width: | Height: | Size: 340 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSUbiquitousContainers</key>
|
||||||
|
<dict>
|
||||||
|
<key>iCloud.io.nekohasekai.sfa</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSUbiquitousContainerIsDocumentScopePublic</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSUbiquitousContainerName</key>
|
||||||
|
<string>sing-box</string>
|
||||||
|
<key>NSUbiquitousContainerSupportedFolderLevels</key>
|
||||||
|
<string>Any</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.developer.networking.networkextension</key>
|
||||||
|
<array>
|
||||||
|
<string>packet-tunnel-provider-systemextension</string>
|
||||||
|
</array>
|
||||||
|
<key>com.apple.developer.system-extension.install</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.io.nekohasekai.sfa</string>
|
||||||
|
</array>
|
||||||
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -1,98 +1,11 @@
|
|||||||
import ApplicationLibrary
|
import MacLibrary
|
||||||
import Library
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct Application: App {
|
struct Application: App {
|
||||||
@NSApplicationDelegateAdaptor private var appDelegate: ApplicationDelegate
|
@NSApplicationDelegateAdaptor private var appDelegate: ApplicationDelegate
|
||||||
|
|
||||||
@State private var showMenuBarExtra = false
|
|
||||||
@State private var isMenuPresented = false
|
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
Window("sing-box", id: "main", content: {
|
MacApplication()
|
||||||
MainView()
|
|
||||||
.onAppear {
|
|
||||||
Task.detached {
|
|
||||||
await initialize()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.environment(\.showMenuBarExtra, $showMenuBarExtra)
|
|
||||||
})
|
|
||||||
.commands {
|
|
||||||
if showMenuBarExtra {
|
|
||||||
CommandGroup(replacing: .appTermination) {
|
|
||||||
Button("Quit sing-box") {
|
|
||||||
hide(closeApp: true)
|
|
||||||
}
|
|
||||||
.keyboardShortcut("q", modifiers: [.command])
|
|
||||||
}
|
|
||||||
CommandGroup(replacing: .saveItem) {
|
|
||||||
Button("Close") {
|
|
||||||
hide(closeApp: false)
|
|
||||||
}
|
|
||||||
.keyboardShortcut("w", modifiers: [.command])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SidebarCommands()
|
|
||||||
}
|
|
||||||
|
|
||||||
Window("New Profile", id: NewProfileView.windowID) {
|
|
||||||
NewProfileView()
|
|
||||||
}
|
|
||||||
|
|
||||||
WindowGroup("Edit Profile", id: EditProfileWindowView.windowID, for: Int64.self) { profileID in
|
|
||||||
EditProfileWindowView(profileID.wrappedValue)
|
|
||||||
}.commandsRemoved()
|
|
||||||
|
|
||||||
WindowGroup("Edit Content", id: EditProfileContentView.windowID, for: EditProfileContentView.Context.self) { context in
|
|
||||||
EditProfileContentView(context.wrappedValue)
|
|
||||||
}.commandsRemoved()
|
|
||||||
|
|
||||||
Window("Service Log", id: ServiceLogView.windowID) {
|
|
||||||
ServiceLogView()
|
|
||||||
}
|
|
||||||
MenuBarExtra(isInserted: $showMenuBarExtra) {
|
|
||||||
MenuView(isMenuPresented: $isMenuPresented)
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "network.badge.shield.half.filled")
|
|
||||||
}
|
|
||||||
.menuBarExtraStyle(.window)
|
|
||||||
.menuBarExtraAccess(isPresented: $isMenuPresented)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func initialize() {
|
|
||||||
let initialShowMenuBarExtra = SharedPreferences.showMenuBarExtra
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
showMenuBarExtra = initialShowMenuBarExtra
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func hide(closeApp: Bool) {
|
|
||||||
Task.detached {
|
|
||||||
if SharedPreferences.menuBarExtraInBackground {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
hide0(closeApp: closeApp)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
if closeApp {
|
|
||||||
NSApp.terminate(nil)
|
|
||||||
} else {
|
|
||||||
NSApp.keyWindow?.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func hide0(closeApp: Bool) {
|
|
||||||
if closeApp || NSApp.keyWindow?.identifier?.rawValue == "main" {
|
|
||||||
let transformState = ProcessApplicationTransformState(kProcessTransformToUIElementApplication)
|
|
||||||
var psn = ProcessSerialNumber(highLongOfPSN: 0, lowLongOfPSN: UInt32(kCurrentProcess))
|
|
||||||
TransformProcessType(&psn, transformState)
|
|
||||||
NSApp.setActivationPolicy(.accessory)
|
|
||||||
}
|
|
||||||
NSApp.keyWindow?.close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,5 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.network.client</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.network.server</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NetworkExtension</key>
|
||||||
|
<dict>
|
||||||
|
<key>NEMachServiceName</key>
|
||||||
|
<string>group.io.nekohasekai.sfa.system</string>
|
||||||
|
<key>NEProviderClasses</key>
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.networkextension.packet-tunnel</key>
|
||||||
|
<string>$(PRODUCT_MODULE_NAME).PacketTunnelProvider</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import Library
|
||||||
|
import NetworkExtension
|
||||||
|
|
||||||
|
class PacketTunnelProvider: ExtensionProvider {
|
||||||
|
override func startTunnel(options: [String: NSObject]?) async throws {
|
||||||
|
guard let usernameObject = options?["username"] else {
|
||||||
|
writeFatalError("missing start options")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let username = usernameObject as! NSString
|
||||||
|
FilePath.sharedDirectory = URL(filePath: "/Users/\(username)/Library/Group Containers/\(FilePath.groupName)")
|
||||||
|
self.username = String(username)
|
||||||
|
try await super.startTunnel(options: options)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.developer.networking.networkextension</key>
|
||||||
|
<array>
|
||||||
|
<string>packet-tunnel-provider-systemextension</string>
|
||||||
|
</array>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.io.nekohasekai.sfa</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import Foundation
|
||||||
|
import Library
|
||||||
|
import NetworkExtension
|
||||||
|
|
||||||
|
Variant.useSystemExtension = true
|
||||||
|
|
||||||
|
autoreleasepool {
|
||||||
|
NEProvider.startSystemExtensionMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchMain()
|
||||||
@@ -7,21 +7,21 @@
|
|||||||
<key>ApplicationLibrary.xcscheme_^#shared#^_</key>
|
<key>ApplicationLibrary.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>2</integer>
|
<integer>1</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Associations (Playground) 1.xcscheme</key>
|
<key>Associations (Playground) 1.xcscheme</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>isShown</key>
|
<key>isShown</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>5</integer>
|
<integer>15</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Associations (Playground) 2.xcscheme</key>
|
<key>Associations (Playground) 2.xcscheme</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>isShown</key>
|
<key>isShown</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>6</integer>
|
<integer>16</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Associations (Playground) 3.xcscheme</key>
|
<key>Associations (Playground) 3.xcscheme</key>
|
||||||
<dict>
|
<dict>
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
<key>isShown</key>
|
<key>isShown</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>4</integer>
|
<integer>14</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>ExtensionMac.xcscheme_^#shared#^_</key>
|
<key>ExtensionMac.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
@@ -66,6 +66,11 @@
|
|||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>5</integer>
|
<integer>5</integer>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>MacLibrary.xcscheme_^#shared#^_</key>
|
||||||
|
<dict>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>5</integer>
|
||||||
|
</dict>
|
||||||
<key>MessageExtension.xcscheme_^#shared#^_</key>
|
<key>MessageExtension.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
@@ -76,14 +81,14 @@
|
|||||||
<key>isShown</key>
|
<key>isShown</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>11</integer>
|
<integer>9</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>MyPlayground (Playground) 2.xcscheme</key>
|
<key>MyPlayground (Playground) 2.xcscheme</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>isShown</key>
|
<key>isShown</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>12</integer>
|
<integer>10</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>MyPlayground (Playground) 3.xcscheme</key>
|
<key>MyPlayground (Playground) 3.xcscheme</key>
|
||||||
<dict>
|
<dict>
|
||||||
@@ -111,7 +116,7 @@
|
|||||||
<key>isShown</key>
|
<key>isShown</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>10</integer>
|
<integer>8</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>SFA.xcscheme_^#shared#^_</key>
|
<key>SFA.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
@@ -123,15 +128,20 @@
|
|||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>0</integer>
|
<integer>0</integer>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>SFM.System.xcscheme_^#shared#^_</key>
|
||||||
|
<dict>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>2</integer>
|
||||||
|
</dict>
|
||||||
<key>SFM.xcscheme_^#shared#^_</key>
|
<key>SFM.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>1</integer>
|
<integer>4</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>SystemExtension.xcscheme_^#shared#^_</key>
|
<key>SystemExtension.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>16</integer>
|
<integer>3</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Test.xcscheme_^#shared#^_</key>
|
<key>Test.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
@@ -143,14 +153,14 @@
|
|||||||
<key>isShown</key>
|
<key>isShown</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>8</integer>
|
<integer>6</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Tour (Playground) 2.xcscheme</key>
|
<key>Tour (Playground) 2.xcscheme</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>isShown</key>
|
<key>isShown</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>9</integer>
|
<integer>7</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Tour (Playground) 3.xcscheme</key>
|
<key>Tour (Playground) 3.xcscheme</key>
|
||||||
<dict>
|
<dict>
|
||||||
@@ -178,21 +188,21 @@
|
|||||||
<key>isShown</key>
|
<key>isShown</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>7</integer>
|
<integer>5</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>TransactionObserver (Playground) 1.xcscheme</key>
|
<key>TransactionObserver (Playground) 1.xcscheme</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>isShown</key>
|
<key>isShown</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>14</integer>
|
<integer>12</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>TransactionObserver (Playground) 2.xcscheme</key>
|
<key>TransactionObserver (Playground) 2.xcscheme</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>isShown</key>
|
<key>isShown</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>15</integer>
|
<integer>13</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>TransactionObserver (Playground) 3.xcscheme</key>
|
<key>TransactionObserver (Playground) 3.xcscheme</key>
|
||||||
<dict>
|
<dict>
|
||||||
@@ -220,7 +230,7 @@
|
|||||||
<key>isShown</key>
|
<key>isShown</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>13</integer>
|
<integer>11</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>mactest.xcscheme_^#shared#^_</key>
|
<key>mactest.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
|||||||