Prepare system extension support

This commit is contained in:
世界
2023-07-24 20:08:02 +08:00
parent 8a5c1a80d1
commit 12bf02070e
44 changed files with 1469 additions and 382 deletions
@@ -1,12 +1,60 @@
import Library
import SwiftUI
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 var body: some View {
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 {
ActiveDashboardView()
} else if let profile = extensionProfile.wrappedValue {
@@ -16,6 +64,6 @@ public struct DashboardView: View {
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))
.disableAutocorrection(true)
.autocorrectionDisabled()
#if os(iOS)
.textInputAutocapitalization(.none)
.background(Color(UIColor.secondarySystemGroupedBackground))
@@ -74,6 +74,27 @@ public struct SettingView: View {
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") {
FormTextItem("Version", version)
+1 -171
View File
@@ -1,174 +1,4 @@
import Foundation
import Libbox
import Library
import NetworkExtension
class PacketTunnelProvider: NEPacketTunnelProvider {
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")
}
}
}
class PacketTunnelProvider: ExtensionProvider {}
+1
View File
@@ -8,6 +8,7 @@ class Database {
if let writer {
return writer
}
try FileManager.default.createDirectory(at: FilePath.sharedDirectory, withIntermediateDirectories: true)
let database = try DatabasePool(path: FilePath.sharedDirectory.appendingPathComponent("settings.db").relativePath)
var migrator = DatabaseMigrator().disablingDeferredForeignKeyChecks()
migrator.eraseDatabaseOnSchemaChange = true
@@ -2,7 +2,7 @@ import Foundation
import Libbox
import NetworkExtension
class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol {
public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol {
private let tunnel: NEPacketTunnelProvider
private let commandServer: LibboxCommandServer
@@ -11,7 +11,7 @@ class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol {
commandServer = logServer
}
func openTun(_ options: LibboxTunOptionsProtocol?, ret0_: UnsafeMutablePointer<Int32>?) throws {
public func openTun(_ options: LibboxTunOptionsProtocol?, ret0_: UnsafeMutablePointer<Int32>?) throws {
guard let options else {
throw NSError(domain: "nil options", code: 0)
}
@@ -105,52 +105,52 @@ class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol {
}
}
func usePlatformAutoDetectControl() -> Bool {
public func usePlatformAutoDetectControl() -> Bool {
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)
}
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)
}
func useProcFS() -> Bool {
public func useProcFS() -> Bool {
false
}
func writeLog(_ message: String?) {
public func writeLog(_ message: String?) {
guard let message else {
return
}
commandServer.writeMessage(message)
}
func usePlatformDefaultInterfaceMonitor() -> Bool {
public func usePlatformDefaultInterfaceMonitor() -> Bool {
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
}
func getInterfaces() throws -> LibboxNetworkInterfaceIteratorProtocol {
public func getInterfaces() throws -> LibboxNetworkInterfaceIteratorProtocol {
throw NSError(domain: "not implemented", code: 0)
}
func underNetworkExtension() -> Bool {
public func underNetworkExtension() -> Bool {
true
}
}
+12
View File
@@ -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()
if Variant.useSystemExtension {
tunnelProtocol.providerBundleIdentifier = "\(FilePath.packageName).system"
} else {
tunnelProtocol.providerBundleIdentifier = "\(FilePath.packageName).extension"
}
tunnelProtocol.serverAddress = "sing-box"
manager.protocolConfiguration = tunnelProtocol
manager.isEnabled = true
+183
View File
@@ -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")
}
}
}
+95
View File
@@ -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
+14 -4
View File
@@ -12,15 +12,25 @@ public enum FilePath {
public extension FilePath {
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("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 {
+9
View File
@@ -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 Library
class ApplicationDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_: Notification) {
open class ApplicationDelegate: NSObject, NSApplicationDelegate {
public func applicationDidFinishLaunching(_: Notification) {
NSLog("Here I stand")
// ServiceNotification.register() // Not work
let event = NSAppleEventManager.shared().currentAppleEvent
@@ -20,6 +20,15 @@ class ApplicationDelegate: NSObject, NSApplicationDelegate {
}
Task.detached {
do {
if Variant.useSystemExtension {
if await SystemExtension.isInstalled() {
if let result = try await SystemExtension.install() {
if result == .willCompleteAfterReboot {
return
}
}
}
}
try await self.postStart(launchedAsLogInItem)
} catch {
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
}
func applicationShouldHandleReopen(_: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
public func applicationShouldHandleReopen(_: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
if !flag, NSApp.activationPolicy() == .accessory {
NSApp.setActivationPolicy(.regular)
NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.dock").first?.activate()
+96
View File
@@ -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()
}
}
+3
View File
@@ -0,0 +1,3 @@
import Foundation
public class MacLibrary {}
@@ -2,7 +2,7 @@ import ApplicationLibrary
import Library
import SwiftUI
struct MainView: View {
public struct MainView: View {
@Environment(\.controlActiveState) private var controlActiveState
@State private var selection = NavigationPage.dashboard
@@ -14,7 +14,8 @@ struct MainView: View {
@State private var serviceNotificationContent = ""
@State private var serviceNotificationPresented = false
var body: some View {
public init() {}
public var body: some View {
NavigationSplitView {
VStack {
SidebarView()
@@ -6,17 +6,21 @@ import MacControlCenterUI
import MenuBarExtraAccess
import SwiftUI
struct MenuView: View {
public struct MenuView: View {
@Environment(\.openWindow) private var openWindow
private static let sliderWidth: CGFloat = 270
@Binding var isMenuPresented: Bool
@Binding private var isMenuPresented: Bool
@State private var isLoading = true
@State private var profile: ExtensionProfile?
var body: some View {
public init(isMenuPresented: Binding<Bool>) {
_isMenuPresented = isMenuPresented
}
public var body: some View {
MacControlCenterMenu(isPresented: $isMenuPresented) {
MenuHeader("sing-box") {
if isLoading {
@@ -2,11 +2,12 @@ import ApplicationLibrary
import Library
import SwiftUI
struct SidebarView: View {
public struct SidebarView: View {
@Environment(\.selection) private var selection
@Environment(\.extensionProfile) private var extensionProfile
var body: some View {
public init() {}
public var body: some View {
viewBuilder {
if let profile = extensionProfile.wrappedValue {
SidebarView0().environmentObject(profile)
+16
View File
@@ -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
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

+6
View File
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
+20
View File
@@ -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>
+22
View File
@@ -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>
+2 -89
View File
@@ -1,98 +1,11 @@
import ApplicationLibrary
import Library
import MacLibrary
import SwiftUI
@main
struct Application: App {
@NSApplicationDelegateAdaptor private var appDelegate: ApplicationDelegate
@State private var showMenuBarExtra = false
@State private var isMenuPresented = false
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()
MacApplication()
}
}
-2
View File
@@ -28,7 +28,5 @@
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>
+16
View File
@@ -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>
+11
View File
@@ -0,0 +1,11 @@
import Foundation
import Library
import NetworkExtension
Variant.useSystemExtension = true
autoreleasepool {
NEProvider.startSystemExtensionMode()
}
dispatchMain()
File diff suppressed because it is too large Load Diff
@@ -7,21 +7,21 @@
<key>ApplicationLibrary.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
<integer>1</integer>
</dict>
<key>Associations (Playground) 1.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>5</integer>
<integer>15</integer>
</dict>
<key>Associations (Playground) 2.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>6</integer>
<integer>16</integer>
</dict>
<key>Associations (Playground) 3.xcscheme</key>
<dict>
@@ -49,7 +49,7 @@
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>4</integer>
<integer>14</integer>
</dict>
<key>ExtensionMac.xcscheme_^#shared#^_</key>
<dict>
@@ -66,6 +66,11 @@
<key>orderHint</key>
<integer>5</integer>
</dict>
<key>MacLibrary.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>5</integer>
</dict>
<key>MessageExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
@@ -76,14 +81,14 @@
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>11</integer>
<integer>9</integer>
</dict>
<key>MyPlayground (Playground) 2.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>12</integer>
<integer>10</integer>
</dict>
<key>MyPlayground (Playground) 3.xcscheme</key>
<dict>
@@ -111,7 +116,7 @@
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>10</integer>
<integer>8</integer>
</dict>
<key>SFA.xcscheme_^#shared#^_</key>
<dict>
@@ -123,15 +128,20 @@
<key>orderHint</key>
<integer>0</integer>
</dict>
<key>SFM.System.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
</dict>
<key>SFM.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
<integer>4</integer>
</dict>
<key>SystemExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>16</integer>
<integer>3</integer>
</dict>
<key>Test.xcscheme_^#shared#^_</key>
<dict>
@@ -143,14 +153,14 @@
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>8</integer>
<integer>6</integer>
</dict>
<key>Tour (Playground) 2.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>9</integer>
<integer>7</integer>
</dict>
<key>Tour (Playground) 3.xcscheme</key>
<dict>
@@ -178,21 +188,21 @@
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>7</integer>
<integer>5</integer>
</dict>
<key>TransactionObserver (Playground) 1.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>14</integer>
<integer>12</integer>
</dict>
<key>TransactionObserver (Playground) 2.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>15</integer>
<integer>13</integer>
</dict>
<key>TransactionObserver (Playground) 3.xcscheme</key>
<dict>
@@ -220,7 +230,7 @@
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>13</integer>
<integer>11</integer>
</dict>
<key>mactest.xcscheme_^#shared#^_</key>
<dict>