refactor: Fix macOS standalone application

This commit is contained in:
世界
2026-01-05 01:21:24 +08:00
parent ef1c924c64
commit 59317f3c79
66 changed files with 2799 additions and 585 deletions
@@ -100,7 +100,7 @@ public class ProfileServer {
try await processProfileContentRequest(data) try await processProfileContentRequest(data)
} }
default: default:
throw NSError(domain: "unexpected message type \(messageType)", code: 0) throw NSError(domain: "ProfileServer", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Unexpected message type \(messageType)")])
} }
} }
@@ -113,7 +113,7 @@ public class ProfileServer {
let profile = try await ProfileManager.get(request!.profileID) let profile = try await ProfileManager.get(request!.profileID)
guard let profile else { guard let profile else {
throw NSError(domain: "profile not found", code: 0) throw NSError(domain: "ProfileServer", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Profile not found")])
} }
let content = LibboxProfileContent() let content = LibboxProfileContent()
content.name = profile.name content.name = profile.name
@@ -19,7 +19,7 @@ import Library
} }
} }
if !success { if !success {
throw NSError(domain: "register task failed", code: 0) throw NSError(domain: "UIProfileUpdateTask", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Register task failed")])
} }
registered = true registered = true
} }
@@ -1,121 +1,8 @@
import Library
@_exported import struct Library.AlertState
import SwiftUI import SwiftUI
public struct AlertState: Equatable {
public var title: String
public var message: String
public var primaryButton: ButtonState?
public var secondaryButton: ButtonState?
public var onDismiss: (() -> Void)?
public struct ButtonState: Equatable {
public var label: String
public var role: ButtonRole?
public var action: (() -> Void)?
public init(label: String, role: ButtonRole? = nil, action: (() -> Void)? = nil) {
self.label = label
self.role = role
self.action = action
}
public static func == (lhs: ButtonState, rhs: ButtonState) -> Bool {
lhs.label == rhs.label && lhs.role == rhs.role
}
public static func `default`(_ label: String, action: (() -> Void)? = nil) -> ButtonState {
ButtonState(label: label, action: action)
}
public static func cancel(_ label: String = String(localized: "Cancel"), action: (() -> Void)? = nil) -> ButtonState {
ButtonState(label: label, role: .cancel, action: action)
}
public static func destructive(_ label: String, action: (() -> Void)? = nil) -> ButtonState {
ButtonState(label: label, role: .destructive, action: action)
}
}
public init(error: Error, dismiss: (() -> Void)? = nil) {
self.init(errorMessage: error.localizedDescription, dismiss: dismiss)
}
public init(errorMessage: String, dismiss: (() -> Void)? = nil) {
title = String(localized: "Error")
message = errorMessage
primaryButton = .default(String(localized: "Ok"), action: dismiss)
secondaryButton = nil
onDismiss = nil
}
public init(title: String, message: String, dismissButton: ButtonState? = nil) {
self.title = title
self.message = message
primaryButton = dismissButton ?? .default(String(localized: "Ok"))
secondaryButton = nil
onDismiss = nil
}
public init(title: String, message: String, primaryButton: ButtonState, secondaryButton: ButtonState) {
self.title = title
self.message = message
self.primaryButton = primaryButton
self.secondaryButton = secondaryButton
onDismiss = nil
}
public init(title: String, message: String, primaryButton: ButtonState, secondaryButton: ButtonState, onDismiss: @escaping () -> Void) {
self.title = title
self.message = message
self.primaryButton = primaryButton
self.secondaryButton = secondaryButton
self.onDismiss = onDismiss
}
public static func == (lhs: AlertState, rhs: AlertState) -> Bool {
lhs.title == rhs.title && lhs.message == rhs.message &&
lhs.primaryButton == rhs.primaryButton && lhs.secondaryButton == rhs.secondaryButton
}
}
public extension View { public extension View {
@ViewBuilder
func alert(_ binding: Binding<AlertState?>) -> some View {
alert(
binding.wrappedValue?.title ?? "",
isPresented: Binding(
get: { binding.wrappedValue != nil },
set: { newValue, _ in
if !newValue {
binding.wrappedValue?.onDismiss?()
binding.wrappedValue = nil
}
}
),
presenting: binding.wrappedValue
) { alertState in
if let secondary = alertState.secondaryButton {
Button(role: alertState.primaryButton?.role) {
alertState.primaryButton?.action?()
} label: {
Text(alertState.primaryButton?.label ?? "Ok")
}
Button(role: secondary.role) {
secondary.action?()
} label: {
Text(secondary.label)
}
} else if let primary = alertState.primaryButton {
Button(role: primary.role) {
primary.action?()
} label: {
Text(primary.label)
}
}
} message: { alertState in
Text(alertState.message)
}
}
@ViewBuilder @ViewBuilder
func alert(_ binding: Binding<AlertState?>, isLoading: Binding<Bool>) -> some View { func alert(_ binding: Binding<AlertState?>, isLoading: Binding<Bool>) -> some View {
alert( alert(
@@ -153,3 +153,15 @@ public func FormNavigationLink(@ViewBuilder destination: () -> some View, @ViewB
}, label: label) }, label: label)
#endif #endif
} }
#if os(macOS)
public func FormNavigationLink(value: some Hashable, @ViewBuilder label: () -> some View) -> some View {
NavigationLink(value: value, label: label)
}
public extension View {
func formNavigationDestination<D: Hashable>(for data: D.Type, @ViewBuilder destination: @escaping (D) -> some View) -> some View {
navigationDestination(for: data, destination: destination)
}
}
#endif
@@ -0,0 +1,250 @@
import Libbox
import Library
import NetworkExtension
import SwiftUI
#if os(macOS)
import CoreLocation
#endif
#if os(macOS)
@MainActor
private final class WIFIStateLocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
var onAuthorizationGranted: (() -> Void)?
private var pendingAuthorizationRequest = false
override init() {
super.init()
manager.delegate = self
}
func requestAuthorizationAndShowWarning() {
let status = manager.authorizationStatus
switch status {
case .notDetermined:
pendingAuthorizationRequest = true
manager.requestAlwaysAuthorization()
case .authorized, .authorizedAlways:
onAuthorizationGranted?()
default:
break
}
}
nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
Task { @MainActor in
guard self.pendingAuthorizationRequest else { return }
self.pendingAuthorizationRequest = false
let status = manager.authorizationStatus
if status == .authorized || status == .authorizedAlways {
self.onAuthorizationGranted?()
}
}
}
}
#endif
@MainActor
public struct GlobalChecksModifier: ViewModifier {
@EnvironmentObject private var environments: ExtensionEnvironments
@Environment(\.importProfile) private var importProfile
@Environment(\.importRemoteProfile) private var importRemoteProfile
@Environment(\.selection) private var selection
@State private var alert: AlertState?
@State private var notStarted = false
@Environment(\.openURL) private var openURL
#if os(macOS)
@StateObject private var wifiLocationManager = WIFIStateLocationManager()
#endif
public init() {}
public func body(content: Content) -> some View {
contentView(content)
.alert($alert)
.onAppear {
handleImportProfile()
handleImportRemoteProfile()
}
.onChangeCompat(of: importProfile.wrappedValue) { _ in
handleImportProfile()
}
.onChangeCompat(of: importRemoteProfile.wrappedValue) { _ in
handleImportRemoteProfile()
}
.onChangeCompat(of: environments.extensionProfile?.status) { status in
handleStatusChange(status)
}
}
@ViewBuilder
private func contentView(_ content: Content) -> some View {
#if os(macOS)
content
.onReceive(NotificationCenter.default.publisher(for: .extensionRequiresWIFIState)) { _ in
handleWiFiStateNotification()
}
.onReceive(NotificationCenter.default.publisher(for: .extensionRequiresHelperService)) { _ in
handleHelperServiceNotification()
}
#else
content
#endif
}
private func handleImportProfile() {
guard let profile = importProfile.wrappedValue else { return }
importProfile.wrappedValue = nil
alert = AlertState(
title: String(localized: "Import Profile"),
message: String(localized: "Are you sure to import profile \(profile.name)?"),
primaryButton: .default(String(localized: "Import")) { [weak environments, selection] in
selection.wrappedValue = .dashboard
Task {
do {
try await profile.importProfile()
} catch {
await MainActor.run {
alert = AlertState(error: error)
}
return
}
environments?.profileUpdate.send()
}
},
secondaryButton: .cancel()
)
}
private func handleImportRemoteProfile() {
guard let remoteProfile = importRemoteProfile.wrappedValue else { return }
importRemoteProfile.wrappedValue = nil
alert = AlertState(
title: String(localized: "Import Remote Profile"),
message: String(localized: "Are you sure to import remote profile \(remoteProfile.name)? You will connect to \(remoteProfile.host) to download the configuration."),
primaryButton: .default(String(localized: "Import")) { [weak environments, selection] in
selection.wrappedValue = .dashboard
environments?.pendingImportRemoteProfile = ImportRemoteProfileRequest(name: remoteProfile.name, url: remoteProfile.url)
},
secondaryButton: .cancel()
)
}
private func handleStatusChange(_ status: NEVPNStatus?) {
guard let status else { return }
switch status {
case .connected:
notStarted = false
Task {
await checkDeprecatedNotes()
}
case .connecting:
notStarted = true
case .disconnected:
if #available(iOS 16.0, macOS 13.0, tvOS 17.0, *) {
if notStarted, let profile = environments.extensionProfile {
Task {
await checkLastDisconnectError(profile: profile)
}
}
}
notStarted = false
default:
break
}
}
@available(iOS 16.0, macOS 13.0, tvOS 17.0, *)
private nonisolated func checkLastDisconnectError(profile: ExtensionProfile) async {
if let alertState = await profile.checkLastDisconnectError() {
await MainActor.run {
alert = alertState
}
}
}
private nonisolated func checkDeprecatedNotes() async {
let disableWarnings = await SharedPreferences.disableDeprecatedWarnings.get()
guard !disableWarnings else { return }
do {
let reports = try LibboxNewStandaloneCommandClient()!.getDeprecatedNotes()
if reports.hasNext() {
await MainActor.run {
showNextDeprecatedNote(reports)
}
}
} catch {
await MainActor.run {
alert = AlertState(error: error)
}
}
}
private func showNextDeprecatedNote(_ reports: any LibboxDeprecatedNoteIteratorProtocol) {
guard reports.hasNext() else { return }
let report = reports.next()!
let continueChain: () -> Void = {
Task.detached {
try? await Task.sleep(nanoseconds: 300 * NSEC_PER_MSEC)
await MainActor.run {
showNextDeprecatedNote(reports)
}
}
}
if report.migrationLink.isEmpty {
var state = AlertState(
title: String(localized: "Deprecated Warning"),
message: report.message(),
dismissButton: .cancel(String(localized: "Ok"))
)
state.onDismiss = continueChain
alert = state
} else {
alert = AlertState(
title: String(localized: "Deprecated Warning"),
message: report.message(),
primaryButton: .default(String(localized: "Documentation")) {
openURL(URL(string: report.migrationLink)!)
},
secondaryButton: .cancel(String(localized: "Ok")),
onDismiss: continueChain
)
}
}
#if os(macOS)
private func handleWiFiStateNotification() {
guard Variant.useSystemExtension else { return }
wifiLocationManager.onAuthorizationGranted = {
alert = AlertState(
title: String(localized: "WiFi State Access"),
message: String(localized: "In the standalone version of SFM, reading WiFi state requires this app to be running. After you quit the SFM app, the sing-box service cannot continue to provide `wifi_ssid` and `wifi_bssid` routing rules.")
)
}
wifiLocationManager.requestAuthorizationAndShowWarning()
}
private func handleHelperServiceNotification() {
guard Variant.useSystemExtension, HelperServiceManager.rootHelperStatus != .enabled else { return }
alert = AlertState(
title: String(localized: "Helper Service Required"),
message: String(localized: "The sing-box service requires Helper Service to provide process lookup functionality, which supports `process_name` and `process_path` routing rules."),
primaryButton: .default(String(localized: "App Settings")) {
NotificationCenter.default.post(name: .navigateToSettingsPage, object: SettingsPage.app)
},
secondaryButton: .cancel()
)
}
#endif
}
public extension View {
func globalChecks() -> some View {
modifier(GlobalChecksModifier())
}
}
@@ -495,10 +495,10 @@ extension ProfileCard {
@Published var profileToEdit: Profile? @Published var profileToEdit: Profile?
@Published var shareItemType: ShareItemType? @Published var shareItemType: ShareItemType?
#if !os(tvOS) #if !os(tvOS)
@Published var profileExportDocument: ProfileExportDocument? @Published var profileExportDocument: ProfileExportDocument?
@Published var showProfileExporter = false @Published var showProfileExporter = false
@Published var profileJSONExportDocument: ProfileJSONExportDocument? @Published var profileJSONExportDocument: ProfileJSONExportDocument?
@Published var showJSONExporter = false @Published var showJSONExporter = false
#endif #endif
#if os(macOS) #if os(macOS)
var shareButtonView: NSView? var shareButtonView: NSView?
@@ -511,10 +511,7 @@ extension ProfileCard {
try await profile.updateRemoteProfile() try await profile.updateRemoteProfile()
environments.profileUpdate.send() environments.profileUpdate.send()
} catch { } catch {
alert = AlertState( alert = AlertState(error: error)
title: String(localized: "Update Failed"),
message: error.localizedDescription
)
} }
} }
} }
@@ -402,10 +402,7 @@ struct ProfilePickerSheet: View {
try await profile.origin.updateRemoteProfile() try await profile.origin.updateRemoteProfile()
environments.profileUpdate.send() environments.profileUpdate.send()
} catch { } catch {
alert = AlertState( alert = AlertState(error: error)
title: String(localized: "Update Failed"),
message: error.localizedDescription
)
} }
} }
@@ -4,12 +4,8 @@ import SwiftUI
@MainActor @MainActor
public struct DashboardView: View { public struct DashboardView: View {
@Environment(\.openURL) private var openURL
@Environment(\.importProfile) private var importProfile
@Environment(\.importRemoteProfile) private var importRemoteProfile
@EnvironmentObject private var environments: ExtensionEnvironments @EnvironmentObject private var environments: ExtensionEnvironments
@StateObject private var coordinator = DashboardViewModel() @StateObject private var coordinator = DashboardViewModel()
@State private var importRemoteProfileRequest: NewProfileView.ImportRequest?
#if os(macOS) #if os(macOS)
@Environment(\.controlActiveState) private var controlActiveState @Environment(\.controlActiveState) private var controlActiveState
@@ -19,24 +15,16 @@ public struct DashboardView: View {
public var body: some View { public var body: some View {
content content
.alert($coordinator.alert)
.onAppear { .onAppear {
coordinator.setOpenURL { openURL($0) }
coordinator.setEnvironments(environments) coordinator.setEnvironments(environments)
#if os(macOS) #if os(macOS)
Task { await coordinator.reload() } Task { await coordinator.reload() }
#endif #endif
handleImportProfile()
handleImportRemoteProfile()
}
.onChangeCompat(of: importProfile.wrappedValue) { _ in
handleImportProfile()
}
.onChangeCompat(of: importRemoteProfile.wrappedValue) { _ in
handleImportRemoteProfile()
} }
#if os(tvOS) #if os(tvOS)
.navigationDestination(item: $importRemoteProfileRequest) { request in .navigationDestination(item: $environments.pendingImportRemoteProfile) { request in
NewProfileView(request) NewProfileView(.init(name: request.name, url: request.url))
.environmentObject(environments) .environmentObject(environments)
.onDisappear { .onDisappear {
environments.profileUpdate.send() environments.profileUpdate.send()
@@ -48,7 +36,7 @@ public struct DashboardView: View {
} }
} }
#else #else
.sheet(item: $importRemoteProfileRequest) { request in .sheet(item: $environments.pendingImportRemoteProfile) { request in
importRemoteProfileSheet(for: request) importRemoteProfileSheet(for: request)
} }
#endif #endif
@@ -60,48 +48,12 @@ public struct DashboardView: View {
#endif #endif
} }
private func handleImportProfile() {
if let profile = importProfile.wrappedValue {
importProfile.wrappedValue = nil
coordinator.alert = AlertState(
title: String(localized: "Import Profile"),
message: String(localized: "Are you sure to import profile \(profile.name)?"),
primaryButton: .default(String(localized: "Import")) {
Task {
do {
try await profile.importProfile()
} catch {
coordinator.alert = AlertState(error: error)
return
}
environments.profileUpdate.send()
}
},
secondaryButton: .cancel()
)
}
}
private func handleImportRemoteProfile() {
if let remoteProfile = importRemoteProfile.wrappedValue {
importRemoteProfile.wrappedValue = nil
coordinator.alert = AlertState(
title: String(localized: "Import Remote Profile"),
message: String(localized: "Are you sure to import remote profile \(remoteProfile.name)? You will connect to \(remoteProfile.host) to download the configuration."),
primaryButton: .default(String(localized: "Import")) {
importRemoteProfileRequest = .init(name: remoteProfile.name, url: remoteProfile.url)
},
secondaryButton: .cancel()
)
}
}
@ViewBuilder @ViewBuilder
private func importRemoteProfileSheet(for request: NewProfileView.ImportRequest) -> some View { private func importRemoteProfileSheet(for request: ImportRemoteProfileRequest) -> some View {
NavigationSheet(title: "Import Profile", onDismiss: { NavigationSheet(title: "Import Profile", onDismiss: {
environments.profileUpdate.send() environments.profileUpdate.send()
}, content: { }, content: {
NewProfileView(request) NewProfileView(.init(name: request.name, url: request.url))
.environmentObject(environments) .environmentObject(environments)
}) })
} }
@@ -132,9 +84,13 @@ public struct DashboardView: View {
} else if let profile = environments.extensionProfile { } else if let profile = environments.extensionProfile {
activeDashboardView activeDashboardView
.environmentObject(profile) .environmentObject(profile)
.alert($coordinator.alert)
.onChangeCompat(of: profile.status) { status in .onChangeCompat(of: profile.status) { status in
coordinator.handleStatusChange(status, profile: profile) #if os(macOS)
if Variant.useSystemExtension, status == .connected {
UserServiceEndpointPublisher.shared.refreshEndpointRegistration()
UserServiceEndpointPublisher.shared.checkExtensionRequirements()
}
#endif
} }
} else { } else {
FormView { FormView {
@@ -2,12 +2,15 @@ import Foundation
import Libbox import Libbox
import Library import Library
import NetworkExtension import NetworkExtension
import os
import SwiftUI import SwiftUI
#if os(macOS) #if os(macOS)
import AppKit import AppKit
#endif #endif
private let logger = Logger(category: "DashboardViewModel")
@MainActor @MainActor
public final class DashboardViewModel: BaseViewModel { public final class DashboardViewModel: BaseViewModel {
@Published public var profileList: [ProfilePreview] = [] @Published public var profileList: [ProfilePreview] = []
@@ -15,14 +18,12 @@ public final class DashboardViewModel: BaseViewModel {
@Published public var selection = DashboardPage.overview @Published public var selection = DashboardPage.overview
@Published public var systemProxyAvailable = false @Published public var systemProxyAvailable = false
@Published public var systemProxyEnabled = false @Published public var systemProxyEnabled = false
@Published public var notStarted = false
#if os(macOS) #if os(macOS)
@Published public var systemExtensionInstalled = true @Published public var systemExtensionInstalled = true
#endif #endif
private weak var environments: ExtensionEnvironments? private weak var environments: ExtensionEnvironments?
private var openURL: ((URL) -> Void)?
public func setEnvironments(_ environments: ExtensionEnvironments) { public func setEnvironments(_ environments: ExtensionEnvironments) {
self.environments = environments self.environments = environments
@@ -33,10 +34,6 @@ public final class DashboardViewModel: BaseViewModel {
isLoading = true isLoading = true
} }
public func setOpenURL(_ openURL: @escaping (URL) -> Void) {
self.openURL = openURL
}
public func reload() async { public func reload() async {
#if os(macOS) #if os(macOS)
if Variant.useSystemExtension { if Variant.useSystemExtension {
@@ -86,84 +83,13 @@ public final class DashboardViewModel: BaseViewModel {
systemProxyAvailable = status.available systemProxyAvailable = status.available
systemProxyEnabled = status.enabled systemProxyEnabled = status.enabled
} catch { } catch {
NSLog("reloadSystemProxy: \(error)") logger.debug("reloadSystemProxy: \(error)")
} }
} }
public func updateSelectedProfile() async { public func updateSelectedProfile() async {
selectedProfileID = await SharedPreferences.selectedProfileID.get() selectedProfileID = await SharedPreferences.selectedProfileID.get()
} }
public func handleStatusChange(_ status: NEVPNStatus, profile: ExtensionProfile) {
if status == .connected {
notStarted = false
Task { await checkDeprecatedNotes() }
} else if status == .connecting {
notStarted = true
} else if status == .disconnected {
if #available(iOS 16.0, macOS 13.0, tvOS 17.0, *) {
if notStarted {
Task { await checkLastDisconnectError(profile: profile) }
}
}
}
}
nonisolated func checkDeprecatedNotes() async {
let disableWarnings = await SharedPreferences.disableDeprecatedWarnings.get()
guard !disableWarnings else { return }
do {
let reports = try LibboxNewStandaloneCommandClient()!.getDeprecatedNotes()
if reports.hasNext() {
await MainActor.run {
loopShowDeprecateNotes(reports)
}
}
} catch {
NSLog("checkDeprecatedNotes: \(error)")
}
}
private func loopShowDeprecateNotes(_ reports: any LibboxDeprecatedNoteIteratorProtocol) {
guard reports.hasNext() else { return }
let report = reports.next()!
let continueChain: () -> Void = { [weak self] in
_ = Task.detached {
try? await Task.sleep(nanoseconds: 300 * NSEC_PER_MSEC)
await self?.loopShowDeprecateNotes(reports)
}
}
if report.migrationLink.isEmpty {
alert = AlertState(
title: String(localized: "Deprecated Warning"),
message: report.message(),
dismissButton: .cancel(String(localized: "Ok"))
)
alert?.onDismiss = continueChain
} else {
alert = AlertState(
title: String(localized: "Deprecated Warning"),
message: report.message(),
primaryButton: .default(String(localized: "Documentation")) {
self.openURL?(URL(string: report.migrationLink)!)
},
secondaryButton: .cancel(String(localized: "Ok")),
onDismiss: continueChain
)
}
}
@available(iOS 16.0, macOS 13.0, tvOS 17.0, *)
nonisolated func checkLastDisconnectError(profile: ExtensionProfile) async {
if let alertState = await profile.checkLastDisconnectError() {
await MainActor.run {
alert = alertState
}
}
}
} }
@available(iOS 16.0, macOS 13.0, tvOS 17.0, *) @available(iOS 16.0, macOS 13.0, tvOS 17.0, *)
@@ -184,7 +110,7 @@ extension ExtensionProfile {
) )
} }
#endif #endif
return AlertState(title: String(localized: "Service Error"), message: nsError.localizedDescription) return AlertState(error: nsError)
} }
} }
@@ -13,7 +13,7 @@ public final class OverviewViewModel: BaseViewModel {
if profile.status.isConnected { if profile.status.isConnected {
do { do {
try await serviceReload() try await profile.reloadService()
} catch { } catch {
alert = AlertState(error: error) alert = AlertState(error: error)
} }
@@ -21,10 +21,6 @@ public final class OverviewViewModel: BaseViewModel {
reasserting = false reasserting = false
} }
public nonisolated func serviceReload() async throws {
try LibboxNewStandaloneCommandClient()!.serviceReload()
}
public nonisolated func setSystemProxyEnabled(_ enabled: Bool, profile: ExtensionProfile) async { public nonisolated func setSystemProxyEnabled(_ enabled: Bool, profile: ExtensionProfile) async {
do { do {
await SharedPreferences.systemProxyEnabled.set(enabled) await SharedPreferences.systemProxyEnabled.set(enabled)
+6 -2
View File
@@ -86,7 +86,9 @@ private struct LogViewContent: View {
let button = UIButton(type: .system) let button = UIButton(type: .system)
let config = UIImage.SymbolConfiguration(scale: .large) let config = UIImage.SymbolConfiguration(scale: .large)
button.setImage(UIImage(systemName: "line.3.horizontal.circle", withConfiguration: config), for: .normal) button.setImage(UIImage(systemName: "line.3.horizontal.circle", withConfiguration: config), for: .normal)
button.tintColor = colorScheme == .dark ? .white : .black if #available(iOS 17.0, *) {
button.tintColor = colorScheme == .dark ? .white : .black
}
button.showsMenuAsPrimaryAction = true button.showsMenuAsPrimaryAction = true
button.menu = createMenu() button.menu = createMenu()
button.setContentHuggingPriority(.required, for: .horizontal) button.setContentHuggingPriority(.required, for: .horizontal)
@@ -96,7 +98,9 @@ private struct LogViewContent: View {
func updateUIView(_ uiView: UIButton, context _: Context) { func updateUIView(_ uiView: UIButton, context _: Context) {
uiView.menu = createMenu() uiView.menu = createMenu()
uiView.tintColor = colorScheme == .dark ? .white : .black if #available(iOS 17.0, *) {
uiView.tintColor = colorScheme == .dark ? .white : .black
}
} }
private func createMenu() -> UIMenu { private func createMenu() -> UIMenu {
@@ -86,9 +86,11 @@ public class LogDataModel: ObservableObject {
lastProcessedLogCount = 0 lastProcessedLogCount = 0
lastEffectiveLevel = nil lastEffectiveLevel = nil
lastSearchText = "" lastSearchText = ""
filteredLogs = []
visibleLogs = []
commandClient.clearLogs()
Task.detached { Task.detached {
let client = LibboxNewStandaloneCommandClient() try? LibboxNewStandaloneCommandClient()!.clearLogs()
try? client?.clearLogs()
} }
} }
@@ -39,10 +39,10 @@ public final class EditProfileContentViewModel: BaseViewModel {
private nonisolated func loadContentBackground() async throws { private nonisolated func loadContentBackground() async throws {
guard let profileID else { guard let profileID else {
throw NSError(domain: "Context destroyed", code: 0) throw NSError(domain: "EditProfileContentViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Context destroyed")])
} }
guard let profile = try await ProfileManager.get(profileID) else { guard let profile = try await ProfileManager.get(profileID) else {
throw NSError(domain: "Profile missing", code: 0) throw NSError(domain: "EditProfileContentViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Profile missing")])
} }
let profileContent = try profile.read() let profileContent = try profile.read()
await MainActor.run { await MainActor.run {
@@ -61,7 +61,7 @@
do { do {
message = try socket.read() message = try socket.read()
} catch { } catch {
throw NSError(domain: "read from connection: \(error.localizedDescription)", code: 0) throw NSError(domain: "ImportProfileViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Read from connection: \(error.localizedDescription)")])
} }
var error: NSError? var error: NSError?
switch Int64(message[0]) { switch Int64(message[0]) {
@@ -71,7 +71,7 @@
throw error throw error
} }
if let message { if let message {
throw NSError(domain: "remote error: \(message.message)", code: 0) throw NSError(domain: "ImportProfileViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Remote error: \(message.message)")])
} }
case LibboxMessageTypeProfileList: case LibboxMessageTypeProfileList:
let decoder = LibboxProfileDecoder() let decoder = LibboxProfileDecoder()
@@ -97,7 +97,7 @@
try await importProfile(content!, environments: environments) try await importProfile(content!, environments: environments)
return return
default: default:
throw NSError(domain: "unknown message type \(message[0])", code: 0) throw NSError(domain: "ImportProfileViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Unknown message type \(message[0])")])
} }
} }
} }
@@ -243,17 +243,11 @@ public struct NewProfileMenuView: View {
var error: NSError? var error: NSError?
let remoteProfile = LibboxParseRemoteProfileImportLink(string, &error) let remoteProfile = LibboxParseRemoteProfileImportLink(string, &error)
if let error { if let error {
alert = AlertState( alert = AlertState(error: error)
title: String(localized: "Invalid QR Code"),
message: error.localizedDescription
)
return return
} }
guard let remoteProfile else { guard let remoteProfile else {
alert = AlertState( alert = AlertState(errorMessage: String(localized: "The QR code does not contain a valid profile import link."))
title: String(localized: "Invalid QR Code"),
message: String(localized: "The QR code does not contain a valid profile import link.")
)
return return
} }
importRequest = NewProfileView.ImportRequest(name: remoteProfile.name, url: remoteProfile.url) importRequest = NewProfileView.ImportRequest(name: remoteProfile.name, url: remoteProfile.url)
@@ -111,10 +111,10 @@ public final class NewProfileViewModel: BaseViewModel {
let profileConfig = profileConfigDirectory.appendingPathComponent("config_\(nextProfileID).json") let profileConfig = profileConfigDirectory.appendingPathComponent("config_\(nextProfileID).json")
if fileImport { if fileImport {
guard let fileURL else { guard let fileURL else {
throw NSError(domain: "Missing file", code: 0) throw NSError(domain: "NewProfileViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Missing file")])
} }
if !fileURL.startAccessingSecurityScopedResource() { if !fileURL.startAccessingSecurityScopedResource() {
throw NSError(domain: "Missing access to selected file", code: 0) throw NSError(domain: "NewProfileViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Missing access to selected file")])
} }
defer { defer {
fileURL.stopAccessingSecurityScopedResource() fileURL.stopAccessingSecurityScopedResource()
@@ -48,54 +48,54 @@ public struct QRSDisplayView: View {
Text(String(localized: "FPS")) Text(String(localized: "FPS"))
Spacer() Spacer()
#if os(tvOS) #if os(tvOS)
Button { Button {
fps = max(1, fps - 1) fps = max(1, fps - 1)
} label: { } label: {
Image(systemName: "minus") Image(systemName: "minus")
} }
Text(verbatim: "\(Int(fps))") Text(verbatim: "\(Int(fps))")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.frame(minWidth: 50) .frame(minWidth: 50)
Button { Button {
fps = min(60, fps + 1) fps = min(60, fps + 1)
} label: { } label: {
Image(systemName: "plus") Image(systemName: "plus")
} }
#else #else
Text(verbatim: "\(Int(fps))") Text(verbatim: "\(Int(fps))")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
#endif #endif
} }
#if !os(tvOS) #if !os(tvOS)
Slider(value: $fps, in: 1 ... 60, step: 1) Slider(value: $fps, in: 1 ... 60, step: 1)
#endif #endif
HStack { HStack {
Text(String(localized: "Slice Size")) Text(String(localized: "Slice Size"))
Spacer() Spacer()
#if os(tvOS) #if os(tvOS)
Button { Button {
sliceSize = max(100, sliceSize - 100) sliceSize = max(100, sliceSize - 100)
} label: { } label: {
Image(systemName: "minus") Image(systemName: "minus")
} }
Text("\(Int(sliceSize))") Text("\(Int(sliceSize))")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.frame(minWidth: 50) .frame(minWidth: 50)
Button { Button {
sliceSize = min(1500, sliceSize + 100) sliceSize = min(1500, sliceSize + 100)
} label: { } label: {
Image(systemName: "plus") Image(systemName: "plus")
} }
#else #else
Text("\(Int(sliceSize))") Text("\(Int(sliceSize))")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
#endif #endif
} }
#if !os(tvOS) #if !os(tvOS)
Slider(value: $sliceSize, in: 100 ... 1500, step: 100) Slider(value: $sliceSize, in: 100 ... 1500, step: 100)
#endif #endif
} }
.padding(.horizontal) .padding(.horizontal)
@@ -130,9 +130,9 @@ public struct QRSDisplayView: View {
.padding(.horizontal) .padding(.horizontal)
} }
#if os(macOS) #if os(macOS)
.padding() .padding()
#else #else
.padding([.horizontal, .bottom]) .padding([.horizontal, .bottom])
#endif #endif
.onAppear { .onAppear {
setupGenerator() setupGenerator()
@@ -145,10 +145,7 @@
message: String(localized: "Please enable camera access in Settings to scan QR codes.") message: String(localized: "Please enable camera access in Settings to scan QR codes.")
) )
default: default:
alert = AlertState( alert = AlertState(error: error)
title: String(localized: "Scanner Error"),
message: error.localizedDescription
)
} }
} }
} }
+88 -16
View File
@@ -4,6 +4,8 @@ import SwiftUI
#if os(iOS) #if os(iOS)
import FileProvider import FileProvider
import UIKit import UIKit
#elseif os(macOS)
import ServiceManagement
#endif #endif
@MainActor @MainActor
@@ -15,7 +17,11 @@ public struct CoreView: View {
@State private var disableDeprecatedWarnings = false @State private var disableDeprecatedWarnings = false
@State private var version = "" @State private var version = ""
@State private var dataSize = "" @State private var dataSize: String?
#if os(macOS)
@State private var helperUnavailable = false
#endif
public init() {} public init() {}
public var body: some View { public var body: some View {
@@ -29,7 +35,21 @@ public struct CoreView: View {
} else { } else {
FormView { FormView {
FormTextItem("Version", version) FormTextItem("Version", version)
FormTextItem("Data Size", dataSize) if let dataSize {
FormTextItem("Data Size", dataSize)
} else {
#if os(macOS)
HStack {
Text("Data Size")
Spacer()
Text("Unavailable")
.foregroundStyle(.red)
.onTapGesture {
alert = helperRequiredAlert()
}
}
#endif
}
if Variant.isBeta { if Variant.isBeta {
Section {} Section {}
@@ -40,10 +60,12 @@ public struct CoreView: View {
Section("Working Directory") { Section("Working Directory") {
#if os(macOS) #if os(macOS)
FormButton { if !Variant.useSystemExtension {
NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: FilePath.workingDirectory.relativePath) FormButton {
} label: { NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: FilePath.workingDirectory.relativePath)
Label("Open", systemImage: "macwindow.and.cursorarrow") } label: {
Label("Open", systemImage: "macwindow.and.cursorarrow")
}
} }
#elseif os(iOS) #elseif os(iOS)
if #available(iOS 16.0, *) { if #available(iOS 16.0, *) {
@@ -85,7 +107,11 @@ public struct CoreView: View {
} else { } else {
await MainActor.run { await MainActor.run {
version = LibboxVersion() version = LibboxVersion()
dataSize = "Loading..." #if os(macOS)
if Variant.useSystemExtension {
helperUnavailable = HelperServiceManager.rootHelperStatus != .enabled
}
#endif
isLoading = false isLoading = false
} }
await loadSettingsBackground() await loadSettingsBackground()
@@ -94,7 +120,20 @@ public struct CoreView: View {
private nonisolated func loadSettingsBackground() async { private nonisolated func loadSettingsBackground() async {
let disableDeprecatedWarnings = await SharedPreferences.disableDeprecatedWarnings.get() let disableDeprecatedWarnings = await SharedPreferences.disableDeprecatedWarnings.get()
let dataSize = (try? FilePath.workingDirectory.formattedSize()) ?? "Unknown" let dataSize: String?
#if os(macOS)
if Variant.useSystemExtension {
if let size = try? RootHelperClient.shared.getWorkingDirectorySize() {
dataSize = LibboxFormatBytes(size)
} else {
dataSize = nil
}
} else {
dataSize = (try? FilePath.workingDirectory.formattedSize()) ?? "Unknown"
}
#else
dataSize = (try? FilePath.workingDirectory.formattedSize()) ?? "Unknown"
#endif
await MainActor.run { await MainActor.run {
self.disableDeprecatedWarnings = disableDeprecatedWarnings self.disableDeprecatedWarnings = disableDeprecatedWarnings
self.dataSize = dataSize self.dataSize = dataSize
@@ -102,6 +141,12 @@ public struct CoreView: View {
} }
private func confirmDestroyWorkingDirectory() async { private func confirmDestroyWorkingDirectory() async {
#if os(macOS)
if helperUnavailable {
alert = helperRequiredAlert()
return
}
#endif
if environments.extensionProfile?.status.isConnected == true { if environments.extensionProfile?.status.isConnected == true {
alert = AlertState( alert = AlertState(
title: String(localized: "Service is Running"), title: String(localized: "Service is Running"),
@@ -119,17 +164,44 @@ public struct CoreView: View {
} }
private func stopServiceAndDestroy() async { private func stopServiceAndDestroy() async {
try? await environments.extensionProfile?.stop() do {
await destroyWorkingDirectory() try await environments.extensionProfile!.stop()
} await destroyWorkingDirectory()
} catch {
private nonisolated func destroyWorkingDirectory() async { alert = AlertState(error: error)
try? FileManager.default.removeItem(at: FilePath.workingDirectory)
await MainActor.run {
isLoading = true
} }
} }
private func destroyWorkingDirectory() async {
do {
#if os(macOS)
if Variant.useSystemExtension {
try RootHelperClient.shared.cleanWorkingDirectory()
} else {
try FileManager.default.removeItem(at: FilePath.workingDirectory)
}
#else
try FileManager.default.removeItem(at: FilePath.workingDirectory)
#endif
isLoading = true
} catch {
alert = AlertState(error: error)
}
}
#if os(macOS)
private func helperRequiredAlert() -> AlertState {
AlertState(
title: String(localized: "Helper Service Required"),
message: String(localized: "Managing working directory requires Helper Service."),
primaryButton: .default(String(localized: "App Settings")) {
NotificationCenter.default.post(name: .navigateToSettingsPage, object: SettingsPage.app)
},
secondaryButton: .cancel(String(localized: "Ok"))
)
}
#endif
#if os(iOS) #if os(iOS)
@available(iOS 16.0, *) @available(iOS 16.0, *)
private nonisolated func openInFilesApp() async { private nonisolated func openInFilesApp() async {
@@ -12,6 +12,11 @@ public struct AppView: View {
@State private var startAtLogin = false @State private var startAtLogin = false
@Environment(\.showMenuBarExtra) private var showMenuBarExtra @Environment(\.showMenuBarExtra) private var showMenuBarExtra
@State private var menuBarExtraInBackground = false @State private var menuBarExtraInBackground = false
#if os(macOS)
@State private var rootHelperRegistrationStatus: SMAppService.Status = .notRegistered
#endif
@State private var alert: AlertState? @State private var alert: AlertState?
@@ -67,6 +72,48 @@ public struct AppView: View {
Label("Uninstall", systemImage: "trash.fill").foregroundColor(.red) Label("Uninstall", systemImage: "trash.fill").foregroundColor(.red)
} }
} }
Section {
if rootHelperRegistrationStatus == .enabled {
FormButton {
performHelperAction {
try HelperServiceManager.unregisterRootHelper()
try HelperServiceManager.registerRootHelper()
}
} label: {
Label("Update", systemImage: "arrow.down.doc.fill")
}
FormButton(role: .destructive) {
performHelperAction {
try HelperServiceManager.unregisterRootHelper()
}
} label: {
Label("Uninstall", systemImage: "trash.fill").foregroundColor(.red)
}
} else if rootHelperRegistrationStatus == .requiresApproval {
FormButton {
openHelperSettings()
} label: {
Label("Enable", systemImage: "switch.2")
}
} else {
FormButton {
performHelperAction {
try HelperServiceManager.registerRootHelper()
}
} label: {
Label("Install", systemImage: "square.and.arrow.down.fill")
}
}
} header: {
VStack(alignment: .leading, spacing: 4) {
Text("Helper Service")
Text("This helper service provides process lookup for `process_name` and `process_path` routing rules, and manages the working directory.")
.font(.caption)
.foregroundStyle(.secondary)
.textCase(nil)
}
}
} }
#endif #endif
} }
@@ -83,6 +130,9 @@ public struct AppView: View {
#if os(macOS) #if os(macOS)
startAtLogin = SMAppService.mainApp.status == .enabled startAtLogin = SMAppService.mainApp.status == .enabled
menuBarExtraInBackground = await SharedPreferences.menuBarExtraInBackground.get() menuBarExtraInBackground = await SharedPreferences.menuBarExtraInBackground.get()
if Variant.useSystemExtension {
refreshHelperStatus()
}
#endif #endif
isLoading = false isLoading = false
} }
@@ -147,5 +197,31 @@ public struct AppView: View {
} }
} }
private func performHelperAction(_ action: () throws -> Void) {
do {
try action()
refreshHelperStatus()
} catch {
alert = AlertState(error: error)
}
}
private func refreshHelperStatus() {
rootHelperRegistrationStatus = HelperServiceManager.rootHelperStatus
}
private func openHelperSettings() {
if #available(macOS 13.0, *) {
SMAppService.openSystemSettingsLoginItems()
return
}
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.users?LoginItems"),
NSWorkspace.shared.open(url)
{
return
}
NSWorkspace.shared.open(URL(fileURLWithPath: "/System/Applications/System Preferences.app"))
}
#endif #endif
} }
@@ -296,7 +296,7 @@ public struct OnDemandRulesView: View {
await SharedPreferences.onDemandRules.set(rules) await SharedPreferences.onDemandRules.set(rules)
let savedRules = await SharedPreferences.onDemandRules.get() let savedRules = await SharedPreferences.onDemandRules.get()
if savedRules != rules { if savedRules != rules {
alert = AlertState(errorMessage: "Failed to save rules") alert = AlertState(errorMessage: String(localized: "Failed to save rules"))
return return
} }
await updateService() await updateService()
@@ -19,7 +19,7 @@ struct PacketTunnelView: View {
Group { Group {
if isLoading { if isLoading {
ProgressView().onAppear { ProgressView().onAppear {
Task.detached { Task {
await loadSettings() await loadSettings()
} }
} }
@@ -115,6 +115,7 @@ struct PacketTunnelView: View {
} }
} }
@MainActor
private func loadSettings() async { private func loadSettings() async {
ignoreMemoryLimit = await SharedPreferences.ignoreMemoryLimit.get() ignoreMemoryLimit = await SharedPreferences.ignoreMemoryLimit.get()
#if !os(tvOS) #if !os(tvOS)
@@ -1,4 +1,3 @@
import Libbox
import Library import Library
import SwiftUI import SwiftUI
@@ -15,7 +14,7 @@ public struct ProfileOverrideView: View {
Group { Group {
if isLoading { if isLoading {
ProgressView().onAppear { ProgressView().onAppear {
Task.detached { Task {
await loadSettings() await loadSettings()
} }
} }
@@ -64,12 +63,13 @@ public struct ProfileOverrideView: View {
return return
} }
do { do {
try LibboxNewStandaloneCommandClient()?.serviceReload() try await profile.reloadService()
} catch { } catch {
alert = AlertState(error: error) alert = AlertState(error: error)
} }
} }
@MainActor
private func loadSettings() async { private func loadSettings() async {
excludeDefaultRoute = await SharedPreferences.excludeDefaultRoute.get() excludeDefaultRoute = await SharedPreferences.excludeDefaultRoute.get()
autoRouteUseSubRangesByDefault = await SharedPreferences.autoRouteUseSubRangesByDefault.get() autoRouteUseSubRangesByDefault = await SharedPreferences.autoRouteUseSubRangesByDefault.get()
@@ -1,6 +1,26 @@
import Library import Library
import SwiftUI import SwiftUI
#if os(macOS)
private struct SettingsNavigationPathKey: EnvironmentKey {
static let defaultValue: Binding<NavigationPath>? = nil
}
public extension EnvironmentValues {
var settingsNavigationPath: Binding<NavigationPath>? {
get { self[SettingsNavigationPathKey.self] }
set { self[SettingsNavigationPathKey.self] = newValue }
}
}
#endif
#if os(macOS)
public enum SettingsPage: Hashable {
case app
case core, packetTunnel, onDemandRules, profileOverride, sponsors
}
#endif
public struct SettingView: View { public struct SettingView: View {
private enum Tabs: Int, CaseIterable, Identifiable { private enum Tabs: Int, CaseIterable, Identifiable {
var id: Self { var id: Self {
@@ -13,6 +33,25 @@ public struct SettingView: View {
case core, packetTunnel, onDemandRules, profileOverride, sponsors case core, packetTunnel, onDemandRules, profileOverride, sponsors
#if os(macOS)
var page: SettingsPage {
switch self {
case .app:
return .app
case .core:
return .core
case .packetTunnel:
return .packetTunnel
case .onDemandRules:
return .onDemandRules
case .profileOverride:
return .profileOverride
case .sponsors:
return .sponsors
}
}
#endif
var label: some View { var label: some View {
Label(title, systemImage: iconImage) Label(title, systemImage: iconImage)
} }
@@ -83,16 +122,45 @@ public struct SettingView: View {
@MainActor @MainActor
var navigationLink: some View { var navigationLink: some View {
FormNavigationLink { #if os(macOS)
contentView FormNavigationLink(value: page) {
} label: { label
label }
} #else
FormNavigationLink {
contentView
} label: {
label
}
#endif
} }
} }
@StateObject private var viewModel = SettingViewModel() #if os(macOS)
@MainActor
@ViewBuilder
private static func destinationView(for page: SettingsPage) -> some View {
Group {
switch page {
case .app:
AppView()
case .core:
CoreView()
case .packetTunnel:
PacketTunnelView()
case .onDemandRules:
OnDemandRulesView()
case .profileOverride:
ProfileOverrideView()
case .sponsors:
SponsorsView()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
}
#endif
@StateObject private var viewModel = SettingViewModel()
public init() {} public init() {}
public var body: some View { public var body: some View {
FormView { FormView {
@@ -159,5 +227,10 @@ public struct SettingView: View {
} }
} }
} }
#if os(macOS)
.formNavigationDestination(for: SettingsPage.self) { page in
Self.destinationView(for: page)
}
#endif
} }
} }
@@ -93,6 +93,14 @@ class FileProviderItem: NSObject, NSFileProviderItem {
return NSNumber(value: contents?.count ?? 0) return NSNumber(value: contents?.count ?? 0)
} }
// MARK: - Local File Status
var isUploaded: Bool { true }
var isUploading: Bool { false }
var isDownloaded: Bool { true }
var isDownloading: Bool { false }
var isMostRecentVersionDownloaded: Bool { true }
private var isDirectory: Bool { private var isDirectory: Bool {
var isDir: ObjCBool = false var isDir: ObjCBool = false
FileManager.default.fileExists(atPath: url.path, isDirectory: &isDir) FileManager.default.fileExists(atPath: url.path, isDirectory: &isDir)
+191
View File
@@ -0,0 +1,191 @@
import Darwin
import Foundation
import os
private let PROC_PIDPATHINFO_MAXSIZE: Int32 = 4096
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ConnectionOwnerLookup")
enum ConnectionOwnerLookup {
struct Result {
let userId: Int32
let userName: String
let processPath: String
}
static func find(
ipProtocol: Int32,
sourceAddress: String,
sourcePort: Int32,
destinationAddress: String,
destinationPort: Int32
) -> Result? {
let sourceAddr = parseAddress(sourceAddress)
let destAddr = parseAddress(destinationAddress)
guard let sourceAddr, let destAddr else {
logger.error("find: failed to parse addresses")
return nil
}
let pidCount = proc_listpids(UInt32(PROC_ALL_PIDS), 0, nil, 0)
guard pidCount > 0 else {
logger.error("find: no processes found")
return nil
}
let pidBufferSize = Int(pidCount) * MemoryLayout<pid_t>.size
let pids = UnsafeMutablePointer<pid_t>.allocate(capacity: Int(pidCount))
defer { pids.deallocate() }
let actualCount = proc_listpids(UInt32(PROC_ALL_PIDS), 0, pids, Int32(pidBufferSize))
guard actualCount > 0 else {
logger.error("find: failed to list processes")
return nil
}
let numPids = Int(actualCount) / MemoryLayout<pid_t>.size
for i in 0 ..< numPids {
let pid = pids[i]
if pid == 0 { continue }
if let result = checkProcessForConnection(
pid: pid,
ipProtocol: ipProtocol,
sourceAddr: sourceAddr,
sourcePort: UInt16(sourcePort),
destAddr: destAddr,
destPort: UInt16(destinationPort)
) {
return result
}
}
return nil
}
private static func checkProcessForConnection(
pid: pid_t,
ipProtocol: Int32,
sourceAddr: Data,
sourcePort: UInt16,
destAddr: Data,
destPort: UInt16
) -> Result? {
let bufferSize = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, nil, 0)
guard bufferSize > 0 else { return nil }
let fdBuffer = UnsafeMutableRawPointer.allocate(byteCount: Int(bufferSize), alignment: MemoryLayout<proc_fdinfo>.alignment)
defer { fdBuffer.deallocate() }
let actualSize = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, fdBuffer, bufferSize)
guard actualSize > 0 else { return nil }
let fdCount = Int(actualSize) / MemoryLayout<proc_fdinfo>.size
for i in 0 ..< fdCount {
let fd = fdBuffer.load(fromByteOffset: i * MemoryLayout<proc_fdinfo>.size, as: proc_fdinfo.self)
guard fd.proc_fdtype == PROX_FDTYPE_SOCKET else { continue }
var socketInfo = socket_fdinfo()
let socketInfoSize = Int32(MemoryLayout<socket_fdinfo>.size)
let result = proc_pidfdinfo(pid, fd.proc_fd, PROC_PIDFDSOCKETINFO, &socketInfo, socketInfoSize)
guard result == socketInfoSize else { continue }
let soi: in_sockinfo
if ipProtocol == IPPROTO_TCP {
guard socketInfo.psi.soi_kind == SOCKINFO_TCP else { continue }
soi = socketInfo.psi.soi_proto.pri_tcp.tcpsi_ini
} else if ipProtocol == IPPROTO_UDP {
guard socketInfo.psi.soi_kind == SOCKINFO_IN else { continue }
soi = socketInfo.psi.soi_proto.pri_in
} else {
continue
}
if matchesConnection(
socketInfo: soi,
sourceAddr: sourceAddr,
sourcePort: sourcePort,
destAddr: destAddr,
destPort: destPort
) {
return getProcessInfo(pid: pid)
}
}
return nil
}
private static func matchesConnection(
socketInfo: in_sockinfo,
sourceAddr: Data,
sourcePort: UInt16,
destAddr: Data,
destPort: UInt16
) -> Bool {
let localPort = UInt16(bigEndian: UInt16(truncatingIfNeeded: socketInfo.insi_lport))
let remotePort = UInt16(bigEndian: UInt16(truncatingIfNeeded: socketInfo.insi_fport))
guard localPort == sourcePort, remotePort == destPort else {
return false
}
var localAddr = socketInfo.insi_laddr
var remoteAddr = socketInfo.insi_faddr
let localData: Data
let remoteData: Data
if sourceAddr.count == 4 {
localData = Data(bytes: &localAddr.ina_46.i46a_addr4, count: 4)
remoteData = Data(bytes: &remoteAddr.ina_46.i46a_addr4, count: 4)
} else {
localData = Data(bytes: &localAddr.ina_6, count: 16)
remoteData = Data(bytes: &remoteAddr.ina_6, count: 16)
}
return localData == sourceAddr && remoteData == destAddr
}
private static func getProcessInfo(pid: pid_t) -> Result? {
let pathBuffer = UnsafeMutablePointer<CChar>.allocate(capacity: Int(PROC_PIDPATHINFO_MAXSIZE))
defer { pathBuffer.deallocate() }
let pathLength = proc_pidpath(pid, pathBuffer, UInt32(PROC_PIDPATHINFO_MAXSIZE))
let processPath = pathLength > 0 ? String(cString: pathBuffer) : ""
var info = proc_bsdinfo()
let infoSize = Int32(MemoryLayout<proc_bsdinfo>.size)
let result = proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &info, infoSize)
guard result == infoSize else { return nil }
let uid = Int32(info.pbi_uid)
let userName: String
if let pw = getpwuid(info.pbi_uid) {
userName = String(cString: pw.pointee.pw_name)
} else {
userName = String(uid)
}
return Result(userId: uid, userName: userName, processPath: processPath)
}
private static func parseAddress(_ address: String) -> Data? {
var addr4 = in_addr()
if inet_pton(AF_INET, address, &addr4) == 1 {
return Data(bytes: &addr4, count: 4)
}
var addr6 = in6_addr()
if inet_pton(AF_INET6, address, &addr6) == 1 {
return Data(bytes: &addr6, count: 16)
}
return nil
}
}
+28
View File
@@ -0,0 +1,28 @@
<?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>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>BasePackageIdentifier</key>
<string>$(BASE_PACKAGE_IDENTIFIER)</string>
<key>AppGroupIdentifier</key>
<string>$(APP_GROUP_IDENTIFIER)</string>
</dict>
</plist>
@@ -0,0 +1,26 @@
<?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>Label</key>
<string>io.nekohasekai.sfavt.helper</string>
<key>BundleProgram</key>
<string>Contents/Helpers/RootHelper</string>
<key>MachServices</key>
<dict>
<key>287TTNZF8L.io.nekohasekai.sfavt.helper</key>
<true/>
</dict>
<key>AssociatedBundleIdentifiers</key>
<array>
<string>io.nekohasekai.sfavt.standalone</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
</dict>
</plist>
+12
View File
@@ -0,0 +1,12 @@
<?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.security.app-sandbox</key>
<false/>
<key>com.apple.security.application-groups</key>
<array>
<string>$(TeamIdentifierPrefix)$(BASE_PACKAGE_IDENTIFIER)</string>
</array>
</dict>
</plist>
+109
View File
@@ -0,0 +1,109 @@
import Foundation
import Library
import os
private let logger = Logger(category: "RootHelper")
class RootHelperService: NSObject {
private var listener: NSXPCListener?
func start() {
setupLogging()
startXPCListener()
}
private func setupLogging() {
let basePath = "/var/log/sing-box"
try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true)
let logPath = basePath + "/roothelper.log"
freopen(logPath, "a", stderr)
}
private func startXPCListener() {
let machServiceName = getMachServiceName()
listener = NSXPCListener(machServiceName: machServiceName)
listener?.delegate = self
listener?.resume()
}
private func getMachServiceName() -> String {
if let identifier = Bundle.main.object(forInfoDictionaryKey: "AppGroupIdentifier") as? String {
return "\(identifier).helper"
}
fatalError("Missing AppGroupIdentifier in Info.plist")
}
}
extension RootHelperService: NSXPCListenerDelegate {
func listener(_: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
let allowedBundleIDs = [
AppConfiguration.systemExtensionBundleID,
AppConfiguration.packageName + ".standalone",
]
guard XPCConnectionValidator.validateConnection(
newConnection,
teamID: AppConfiguration.teamID,
allowedBundleIDs: allowedBundleIDs
) else {
let info = XPCConnectionValidator.getConnectionInfo(newConnection)
logger.warning("Rejected XPC connection: pid=\(info.pid), bundleID=\(info.bundleID ?? "unknown"), teamID=\(info.teamID ?? "unknown")")
return false
}
let exportedInterface = NSXPCInterface(with: RootHelperProtocol.self)
RootHelperXPC.configureInterface(exportedInterface)
newConnection.exportedInterface = exportedInterface
newConnection.exportedObject = self
newConnection.resume()
return true
}
}
extension RootHelperService: RootHelperProtocol {
func findConnectionOwner(
ipProtocol: Int32,
sourceAddress: String,
sourcePort: Int32,
destinationAddress: String,
destinationPort: Int32,
reply: @escaping (ConnectionOwnerResult?, NSError?) -> Void
) {
guard let result = ConnectionOwnerLookup.find(
ipProtocol: ipProtocol,
sourceAddress: sourceAddress,
sourcePort: sourcePort,
destinationAddress: destinationAddress,
destinationPort: destinationPort
) else {
let error = NSError(domain: "RootHelper", code: -1, userInfo: [
NSLocalizedDescriptionKey: "Connection owner not found",
])
logger.error("findConnectionOwner: \(error.localizedDescription)")
reply(nil, error)
return
}
let ownerResult = ConnectionOwnerResult(
userId: result.userId,
userName: result.userName,
processPath: result.processPath
)
reply(ownerResult, nil)
}
func getWorkingDirectorySize(reply: @escaping (Int64, NSError?) -> Void) {
let size = WorkingDirectoryManager.getSize()
reply(size, nil)
}
func cleanWorkingDirectory(reply: @escaping (NSError?) -> Void) {
do {
try WorkingDirectoryManager.clean()
reply(nil)
} catch {
logger.error("cleanWorkingDirectory error: \(error.localizedDescription)")
reply(error as NSError)
}
}
}
@@ -0,0 +1,42 @@
import Foundation
import Library
enum WorkingDirectoryManager {
private static var workingDirectoryPath: String {
"/var/root/Library/Containers/\(AppConfiguration.systemExtensionBundleID)/Data/Working"
}
static func getSize() -> Int64 {
let path = workingDirectoryPath
guard FileManager.default.fileExists(atPath: path) else {
return 0
}
var totalSize: Int64 = 0
let enumerator = FileManager.default.enumerator(atPath: path)
while let file = enumerator?.nextObject() as? String {
let filePath = (path as NSString).appendingPathComponent(file)
if let attrs = try? FileManager.default.attributesOfItem(atPath: filePath),
let size = attrs[.size] as? Int64
{
totalSize += size
}
}
return totalSize
}
static func clean() throws {
let path = workingDirectoryPath
guard FileManager.default.fileExists(atPath: path) else {
return
}
let contents = try FileManager.default.contentsOfDirectory(atPath: path)
for item in contents {
let itemPath = (path as NSString).appendingPathComponent(item)
try FileManager.default.removeItem(atPath: itemPath)
}
}
}
+5
View File
@@ -0,0 +1,5 @@
import Foundation
let service = RootHelperService()
service.start()
dispatchMain()
+7 -8
View File
@@ -1,6 +1,5 @@
import AppIntents import AppIntents
import Foundation import Foundation
import Libbox
import Library import Library
struct StartServiceIntent: AppIntent { struct StartServiceIntent: AppIntent {
@@ -18,7 +17,7 @@ struct StartServiceIntent: AppIntent {
func perform() async throws -> some IntentResult & ProvidesDialog { func perform() async throws -> some IntentResult & ProvidesDialog {
guard let extensionProfile = try await (ExtensionProfile.load()) else { guard let extensionProfile = try await (ExtensionProfile.load()) else {
throw NSError(domain: "NetworkExtension not installed", code: 0) throw NSError(domain: "IntentsExtension", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "NetworkExtension not installed")])
} }
let profileList = try await ProfileManager.list() let profileList = try await ProfileManager.list()
let specifiedProfile = profileList.first { $0.name == profile } let specifiedProfile = profileList.first { $0.name == profile }
@@ -30,13 +29,13 @@ struct StartServiceIntent: AppIntent {
profileChanged = true profileChanged = true
} }
} else if profile != "default" { } else if profile != "default" {
throw NSError(domain: "Specified profile not found: \(profile)", code: 0) throw NSError(domain: "IntentsExtension", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Specified profile not found: \(profile)")])
} }
if await extensionProfile.status == .connected { if await extensionProfile.status == .connected {
if !profileChanged { if !profileChanged {
return .result(dialog: "Service is already running") return .result(dialog: "Service is already running")
} }
try LibboxNewStandaloneCommandClient()!.serviceReload() try await extensionProfile.reloadService()
} else if await extensionProfile.status.isConnected { } else if await extensionProfile.status.isConnected {
try await extensionProfile.restart() try await extensionProfile.restart()
} else { } else {
@@ -61,7 +60,7 @@ struct RestartServiceIntent: AppIntent {
return .result(dialog: "Service is not installed") return .result(dialog: "Service is not installed")
} }
if await extensionProfile.status == .connected { if await extensionProfile.status == .connected {
try LibboxNewStandaloneCommandClient()!.serviceReload() try await extensionProfile.reloadService()
} else if await extensionProfile.status.isConnected { } else if await extensionProfile.status.isConnected {
try await extensionProfile.restart() try await extensionProfile.restart()
} else { } else {
@@ -145,7 +144,7 @@ struct GetCurrentProfile: AppIntent {
func perform() async throws -> some IntentResult & ReturnsValue<String> { func perform() async throws -> some IntentResult & ReturnsValue<String> {
guard let profile = try await ProfileManager.get(SharedPreferences.selectedProfileID.get()) else { guard let profile = try await ProfileManager.get(SharedPreferences.selectedProfileID.get()) else {
throw NSError(domain: "No profile selected", code: 0) throw NSError(domain: "IntentsExtension", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "No profile selected")])
} }
return .result(value: profile.name) return .result(value: profile.name)
} }
@@ -167,10 +166,10 @@ struct UpdateProfileIntent: AppIntent {
init() {} init() {}
func perform() async throws -> some IntentResult & ProvidesDialog { func perform() async throws -> some IntentResult & ProvidesDialog {
guard let profile = try await ProfileManager.get(by: profile) else { guard let profile = try await ProfileManager.get(by: profile) else {
throw NSError(domain: "Specified profile not found: \(profile)", code: 0) throw NSError(domain: "IntentsExtension", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Specified profile not found: \(profile)")])
} }
if profile.type != .remote { if profile.type != .remote {
throw NSError(domain: "Specified profile is not a remote profile", code: 0) throw NSError(domain: "IntentsExtension", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Specified profile is not a remote profile")])
} }
try await profile.updateRemoteProfile() try await profile.updateRemoteProfile()
return .result(dialog: "Profile updated") return .result(dialog: "Profile updated")
+1
View File
@@ -10,6 +10,7 @@ public extension Date {
var relativeFormat: String { var relativeFormat: String {
let formatter = RelativeDateTimeFormatter() let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .full formatter.unitsStyle = .full
formatter.dateTimeStyle = .named
return formatter.localizedString(for: self, relativeTo: Date()) return formatter.localizedString(for: self, relativeTo: Date())
} }
} }
+46 -45
View File
@@ -119,61 +119,62 @@ public extension UTType {
} }
#if !os(tvOS) #if !os(tvOS)
// MARK: - FileDocument for Export
public struct ProfileExportDocument: FileDocument { // MARK: - FileDocument for Export
public static var readableContentTypes: [UTType] { [.profile] }
public let data: Data public struct ProfileExportDocument: FileDocument {
public let filename: String public static var readableContentTypes: [UTType] { [.profile] }
public init(content: LibboxProfileContent) throws { public let data: Data
guard let encoded = content.encode() else { public let filename: String
throw NSError(domain: "ProfileExportDocument", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to encode profile"])
public init(content: LibboxProfileContent) throws {
guard let encoded = content.encode() else {
throw NSError(domain: "ProfileExportDocument", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to encode profile"])
}
data = encoded
filename = "\(content.name).bpf"
} }
data = encoded
filename = "\(content.name).bpf"
}
public init(configuration: ReadConfiguration) throws { public init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents else { guard let data = configuration.file.regularFileContents else {
throw CocoaError(.fileReadCorruptFile) throw CocoaError(.fileReadCorruptFile)
}
self.data = data
filename = "profile.bpf"
} }
self.data = data
filename = "profile.bpf"
}
public func fileWrapper(configuration _: WriteConfiguration) throws -> FileWrapper { public func fileWrapper(configuration _: WriteConfiguration) throws -> FileWrapper {
FileWrapper(regularFileWithContents: data) FileWrapper(regularFileWithContents: data)
}
}
public struct ProfileJSONExportDocument: FileDocument {
public static var readableContentTypes: [UTType] { [.json] }
public let content: String
public let filename: String
public init(jsonContent: String, name: String) {
content = jsonContent
filename = "\(name).json"
}
public init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents,
let content = String(data: data, encoding: .utf8)
else {
throw CocoaError(.fileReadCorruptFile)
} }
self.content = content
filename = "profile.json"
} }
public func fileWrapper(configuration _: WriteConfiguration) throws -> FileWrapper { public struct ProfileJSONExportDocument: FileDocument {
guard let data = content.data(using: .utf8) else { public static var readableContentTypes: [UTType] { [.json] }
throw CocoaError(.fileWriteInapplicableStringEncoding)
public let content: String
public let filename: String
public init(jsonContent: String, name: String) {
content = jsonContent
filename = "\(name).json"
}
public init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents,
let content = String(data: data, encoding: .utf8)
else {
throw CocoaError(.fileReadCorruptFile)
}
self.content = content
filename = "profile.json"
}
public func fileWrapper(configuration _: WriteConfiguration) throws -> FileWrapper {
guard let data = content.data(using: .utf8) else {
throw CocoaError(.fileWriteInapplicableStringEncoding)
}
return FileWrapper(regularFileWithContents: data)
} }
return FileWrapper(regularFileWithContents: data)
} }
}
#endif #endif
+1 -1
View File
@@ -29,7 +29,7 @@ public extension Profile {
if await SharedPreferences.selectedProfileID.get() == id { if await SharedPreferences.selectedProfileID.get() == id {
if let profile = try? await ExtensionProfile.load() { if let profile = try? await ExtensionProfile.load() {
if await profile.status == .connected { if await profile.status == .connected {
try LibboxNewStandaloneCommandClient()!.serviceReload() try await profile.reloadService()
} }
} }
} }
@@ -1,6 +1,9 @@
import BinaryCodable import BinaryCodable
import Foundation import Foundation
import GRDB import GRDB
import os
private let logger = Logger(category: "SharedPreferences")
extension SharedPreferences { extension SharedPreferences {
public class Preference<T: Codable> { public class Preference<T: Codable> {
@@ -16,7 +19,7 @@ extension SharedPreferences {
do { do {
return try await SharedPreferences.read(name) ?? defaultValue return try await SharedPreferences.read(name) ?? defaultValue
} catch { } catch {
NSLog("read preferences error: \(error)") logger.error("read preferences error: \(error)")
return defaultValue return defaultValue
} }
} }
@@ -31,7 +34,7 @@ extension SharedPreferences {
do { do {
try await SharedPreferences.write(name, newValue) try await SharedPreferences.write(name, newValue)
} catch { } catch {
NSLog("write preferences error: \(error)") logger.error("write preferences error: \(error)")
} }
} }
} }
+12 -2
View File
@@ -1,5 +1,8 @@
import Foundation import Foundation
import Libbox import Libbox
import os
private let logger = Logger(category: "CommandClient")
public struct LogEntry: Identifiable { public struct LogEntry: Identifiable {
public let id = UUID() public let id = UUID()
@@ -124,6 +127,13 @@ public class CommandClient: ObservableObject {
} }
} }
public func clearLogs() {
logBatchTimer?.cancel()
logBatchTimer = nil
pendingLogs.removeAll()
logList.removeAll()
}
public func filterConnectionsNow() { public func filterConnectionsNow() {
guard let message = rawConnections else { guard let message = rawConnections else {
return return
@@ -222,7 +232,7 @@ public class CommandClient: ObservableObject {
commandClient.isConnected = false commandClient.isConnected = false
} }
if let message { if let message {
NSLog("client disconnected: \(message)") logger.debug("client disconnected: \(message)")
} }
} }
@@ -234,7 +244,7 @@ public class CommandClient: ObservableObject {
func clearLogs() { func clearLogs() {
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
commandClient.logList.removeAll() commandClient.clearLogs()
} }
} }
+215
View File
@@ -0,0 +1,215 @@
#if os(macOS)
import Foundation
import Libbox
import os
private let logger = Logger(category: "CommandXPC")
@objc public protocol CommandXPCProtocol {
func connectToCommandServer(reply: @escaping (FileHandle?, NSError?) -> Void)
func registerUserServiceEndpoint(_ endpoint: NSXPCListenerEndpoint?, reply: @escaping (NSError?) -> Void)
func extensionRequirements(reply: @escaping (Bool, Bool, NSError?) -> Void)
}
class CommandXPCService: NSObject, NSXPCListenerDelegate {
let socketPath: String
var commandServer: LibboxCommandServer?
private let serviceReadyLock = NSLock()
private var _serviceReady = false
private var serviceReadyContinuations: [CheckedContinuation<Void, Never>] = []
init(socketPath: String) {
self.socketPath = socketPath
}
func waitForServiceReady() async {
serviceReadyLock.lock()
if _serviceReady {
serviceReadyLock.unlock()
return
}
await withCheckedContinuation { continuation in
serviceReadyContinuations.append(continuation)
serviceReadyLock.unlock()
}
}
func markServiceReady() {
serviceReadyLock.lock()
_serviceReady = true
let continuations = serviceReadyContinuations
serviceReadyContinuations.removeAll()
serviceReadyLock.unlock()
for continuation in continuations {
continuation.resume()
}
}
func markServiceNotReady() {
serviceReadyLock.lock()
_serviceReady = false
serviceReadyLock.unlock()
}
func listener(_: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
let allowedBundleIDs = [AppConfiguration.packageName + ".standalone"]
guard XPCConnectionValidator.validateConnection(
newConnection,
teamID: AppConfiguration.teamID,
allowedBundleIDs: allowedBundleIDs
) else {
let info = XPCConnectionValidator.getConnectionInfo(newConnection)
logger.warning("Rejected XPC connection: pid=\(info.pid), bundleID=\(info.bundleID ?? "unknown"), teamID=\(info.teamID ?? "unknown")")
return false
}
let exportedInterface = NSXPCInterface(with: CommandXPCProtocol.self)
CommandXPC.configureInterface(exportedInterface)
newConnection.exportedInterface = exportedInterface
newConnection.exportedObject = CommandXPCHandler(service: self)
newConnection.resume()
return true
}
}
private class CommandXPCHandler: NSObject, CommandXPCProtocol {
private let service: CommandXPCService
init(service: CommandXPCService) {
self.service = service
}
func connectToCommandServer(reply: @escaping (FileHandle?, NSError?) -> Void) {
do {
let handle = try connectToUnixSocket(path: service.socketPath)
reply(handle, nil)
} catch {
reply(nil, error as NSError)
}
}
func registerUserServiceEndpoint(_ endpoint: NSXPCListenerEndpoint?, reply: @escaping (NSError?) -> Void) {
if let endpoint {
UserServiceEndpointRegistry.shared.update(endpoint)
} else {
UserServiceEndpointRegistry.shared.clear()
}
reply(nil)
}
func extensionRequirements(reply: @escaping (Bool, Bool, NSError?) -> Void) {
Task {
await service.waitForServiceReady()
guard let commandServer = service.commandServer else {
reply(false, false, NSError(domain: "CommandXPC", code: -1, userInfo: [
NSLocalizedDescriptionKey: "Command server not available",
]))
return
}
let needWIFI = commandServer.needWIFIState()
let needProcess = commandServer.needFindProcess()
reply(needWIFI, needProcess, nil)
}
}
private func connectToUnixSocket(path: String) throws -> FileHandle {
let fd = socket(AF_UNIX, SOCK_STREAM, 0)
guard fd >= 0 else {
throw NSError(domain: "CommandXPC", code: Int(errno), userInfo: [
NSLocalizedDescriptionKey: "Failed to create socket: \(String(cString: strerror(errno)))",
])
}
var addr = sockaddr_un()
addr.sun_family = sa_family_t(AF_UNIX)
let pathSize = MemoryLayout.size(ofValue: addr.sun_path)
withUnsafeMutableBytes(of: &addr.sun_path) { buffer in
_ = path.withCString { cString in
strncpy(buffer.baseAddress!.assumingMemoryBound(to: CChar.self), cString, pathSize - 1)
}
}
let connectResult = withUnsafePointer(to: &addr) { ptr in
ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddrPtr in
connect(fd, sockaddrPtr, socklen_t(MemoryLayout<sockaddr_un>.size))
}
}
guard connectResult >= 0 else {
close(fd)
throw NSError(domain: "CommandXPC", code: Int(errno), userInfo: [
NSLocalizedDescriptionKey: "Failed to connect to \(path): \(String(cString: strerror(errno)))",
])
}
return FileHandle(fileDescriptor: fd, closeOnDealloc: false)
}
}
public class CommandXPCDialer: NSObject, LibboxXPCDialerProtocol {
public static let shared = CommandXPCDialer()
public func dialXPC(_ ret0_: UnsafeMutablePointer<Int32>?) throws {
let semaphore = DispatchSemaphore(value: 0)
var result: Int32 = -1
var resultError: Error?
let machServiceName = AppConfiguration.appGroupID + ".system"
let connection = NSXPCConnection(machServiceName: machServiceName)
let remoteInterface = NSXPCInterface(with: CommandXPCProtocol.self)
CommandXPC.configureInterface(remoteInterface)
connection.remoteObjectInterface = remoteInterface
connection.resume()
let proxy = connection.remoteObjectProxyWithErrorHandler { error in
logger.error("XPC proxy error: \(error.localizedDescription)")
resultError = error
semaphore.signal()
} as! CommandXPCProtocol
proxy.connectToCommandServer { handle, error in
if let error {
logger.error("connectToCommandServer error: \(error.localizedDescription)")
resultError = error
} else if let handle {
result = dup(handle.fileDescriptor)
}
semaphore.signal()
}
semaphore.wait()
connection.invalidate()
if let error = resultError {
throw error
}
if result < 0 {
logger.error("dialXPC failed: No file handle returned")
throw NSError(domain: "CommandXPCDialer", code: -1, userInfo: [
NSLocalizedDescriptionKey: "No file handle returned",
])
}
ret0_?.pointee = result
}
}
public enum CommandXPC {
public static func configureInterface(_ interface: NSXPCInterface) {
let fileHandleClasses = NSSet(array: [FileHandle.self]) as! Set<AnyHashable>
interface.setClasses(
fileHandleClasses,
for: #selector(CommandXPCProtocol.connectToCommandServer(reply:)),
argumentIndex: 0,
ofReply: true
)
let endpointClasses = NSSet(array: [NSXPCListenerEndpoint.self]) as! Set<AnyHashable>
interface.setClasses(
endpointClasses,
for: #selector(CommandXPCProtocol.registerUserServiceEndpoint(_:reply:)),
argumentIndex: 0,
ofReply: false
)
}
}
#endif
+129 -6
View File
@@ -1,12 +1,141 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
public struct AlertState: Equatable {
public var title: String
public var message: String
public var primaryButton: ButtonState?
public var secondaryButton: ButtonState?
public var onDismiss: (() -> Void)?
public struct ButtonState: Equatable {
public var label: String
public var role: ButtonRole?
public var action: (() -> Void)?
public init(label: String, role: ButtonRole? = nil, action: (() -> Void)? = nil) {
self.label = label
self.role = role
self.action = action
}
public static func == (lhs: ButtonState, rhs: ButtonState) -> Bool {
lhs.label == rhs.label && lhs.role == rhs.role
}
public static func `default`(_ label: String, action: (() -> Void)? = nil) -> ButtonState {
ButtonState(label: label, action: action)
}
public static func cancel(_ label: String = String(localized: "Cancel"), action: (() -> Void)? = nil) -> ButtonState {
ButtonState(label: label, role: .cancel, action: action)
}
public static func destructive(_ label: String, action: (() -> Void)? = nil) -> ButtonState {
ButtonState(label: label, role: .destructive, action: action)
}
}
public init(error: Error, dismiss: (() -> Void)? = nil) {
self.init(errorMessage: error.localizedDescription, dismiss: dismiss)
}
public init(errorMessage: String, dismiss: (() -> Void)? = nil) {
title = String(localized: "Error")
message = errorMessage
primaryButton = .default(String(localized: "Ok"), action: dismiss)
secondaryButton = nil
onDismiss = nil
}
public init(title: String, message: String, dismissButton: ButtonState? = nil) {
self.title = title
self.message = message
primaryButton = dismissButton ?? .default(String(localized: "Ok"))
secondaryButton = nil
onDismiss = nil
}
public init(title: String, message: String, primaryButton: ButtonState, secondaryButton: ButtonState) {
self.title = title
self.message = message
self.primaryButton = primaryButton
self.secondaryButton = secondaryButton
onDismiss = nil
}
public init(title: String, message: String, primaryButton: ButtonState, secondaryButton: ButtonState, onDismiss: @escaping () -> Void) {
self.title = title
self.message = message
self.primaryButton = primaryButton
self.secondaryButton = secondaryButton
self.onDismiss = onDismiss
}
public static func == (lhs: AlertState, rhs: AlertState) -> Bool {
lhs.title == rhs.title && lhs.message == rhs.message &&
lhs.primaryButton == rhs.primaryButton && lhs.secondaryButton == rhs.secondaryButton
}
}
public extension View {
@ViewBuilder
func alert(_ binding: Binding<AlertState?>) -> some View {
alert(
binding.wrappedValue?.title ?? "",
isPresented: Binding(
get: { binding.wrappedValue != nil },
set: { newValue, _ in
if !newValue {
binding.wrappedValue?.onDismiss?()
binding.wrappedValue = nil
}
}
),
presenting: binding.wrappedValue
) { alertState in
if let secondary = alertState.secondaryButton {
Button(role: alertState.primaryButton?.role) {
alertState.primaryButton?.action?()
} label: {
Text(alertState.primaryButton?.label ?? "Ok")
}
Button(role: secondary.role) {
secondary.action?()
} label: {
Text(secondary.label)
}
} else if let primary = alertState.primaryButton {
Button(role: primary.role) {
primary.action?()
} label: {
Text(primary.label)
}
}
} message: { alertState in
Text(alertState.message)
}
}
}
public struct ImportRemoteProfileRequest: Hashable, Identifiable {
public var id: String { url }
public let name: String
public let url: String
public init(name: String, url: String) {
self.name = name
self.url = url
}
}
@MainActor @MainActor
public class ExtensionEnvironments: ObservableObject { public class ExtensionEnvironments: ObservableObject {
@Published public var commandClient = CommandClient([.log, .status, .groups, .clashMode, .connections]) @Published public var commandClient = CommandClient([.log, .status, .groups, .clashMode, .connections])
@Published public var extensionProfileLoading = true @Published public var extensionProfileLoading = true
@Published public var extensionProfile: ExtensionProfile? @Published public var extensionProfile: ExtensionProfile?
@Published public var emptyProfiles = false @Published public var emptyProfiles = false
@Published public var pendingImportRemoteProfile: ImportRemoteProfileRequest?
public let profileUpdate = ObjectWillChangePublisher() public let profileUpdate = ObjectWillChangePublisher()
public let selectedProfileUpdate = ObjectWillChangePublisher() public let selectedProfileUpdate = ObjectWillChangePublisher()
@@ -14,12 +143,6 @@ public class ExtensionEnvironments: ObservableObject {
public init() {} public init() {}
nonisolated deinit {
Task { @MainActor in
commandClient.disconnect()
}
}
public func postReload() { public func postReload() {
Task { Task {
await reload() await reload()
@@ -22,14 +22,17 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
private func openTun0(_ options: LibboxTunOptionsProtocol?, _ ret0_: UnsafeMutablePointer<Int32>?) async throws { private func openTun0(_ options: LibboxTunOptionsProtocol?, _ ret0_: UnsafeMutablePointer<Int32>?) async throws {
guard let options else { guard let options else {
throw NSError(domain: "nil options", code: 0) throw NSError(domain: "ExtensionPlatformInterface", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Nil options")])
} }
guard let ret0_ else { guard let ret0_ else {
throw NSError(domain: "nil return pointer", code: 0) throw NSError(domain: "ExtensionPlatformInterface", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Nil return pointer")])
} }
let autoRouteUseSubRangesByDefault = await SharedPreferences.autoRouteUseSubRangesByDefault.get() let prefs = tunnel.overridePreferences ?? ExtensionProvider.OverridePreferences()
let excludeAPNs = await SharedPreferences.excludeAPNsRoute.get() let autoRouteUseSubRangesByDefault = prefs.autoRouteUseSubRangesByDefault
let excludeAPNs = prefs.excludeAPNsRoute
let excludeDefaultRoute = prefs.excludeDefaultRoute
let systemProxyEnabled = prefs.systemProxyEnabled
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1") let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
if options.getAutoRoute() { if options.getAutoRoute() {
@@ -78,7 +81,7 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
let ipv4RoutePrefix = inet4RouteExcludeAddressIterator.next()! let ipv4RoutePrefix = inet4RouteExcludeAddressIterator.next()!
ipv4ExcludeRoutes.append(NEIPv4Route(destinationAddress: ipv4RoutePrefix.address(), subnetMask: ipv4RoutePrefix.mask())) ipv4ExcludeRoutes.append(NEIPv4Route(destinationAddress: ipv4RoutePrefix.address(), subnetMask: ipv4RoutePrefix.mask()))
} }
if await SharedPreferences.excludeDefaultRoute.get(), !ipv4Routes.isEmpty { if excludeDefaultRoute, !ipv4Routes.isEmpty {
if !ipv4ExcludeRoutes.contains(where: { it in if !ipv4ExcludeRoutes.contains(where: { it in
it.destinationAddress == "0.0.0.0" && it.destinationSubnetMask == "255.255.255.254" it.destinationAddress == "0.0.0.0" && it.destinationSubnetMask == "255.255.255.254"
}) { }) {
@@ -134,7 +137,7 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
ipv6ExcludeRoutes.append(NEIPv6Route(destinationAddress: ipv6RoutePrefix.address(), networkPrefixLength: NSNumber(value: ipv6RoutePrefix.prefix()))) ipv6ExcludeRoutes.append(NEIPv6Route(destinationAddress: ipv6RoutePrefix.address(), networkPrefixLength: NSNumber(value: ipv6RoutePrefix.prefix())))
} }
if await SharedPreferences.excludeDefaultRoute.get(), !ipv6Routes.isEmpty { if excludeDefaultRoute, !ipv6Routes.isEmpty {
if !ipv6ExcludeRoutes.contains(where: { it in if !ipv6ExcludeRoutes.contains(where: { it in
it.destinationAddress == "::" && it.destinationNetworkPrefixLength == 127 it.destinationAddress == "::" && it.destinationNetworkPrefixLength == 127
}) { }) {
@@ -152,7 +155,7 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
let proxyServer = NEProxyServer(address: options.getHTTPProxyServer(), port: Int(options.getHTTPProxyServerPort())) let proxyServer = NEProxyServer(address: options.getHTTPProxyServer(), port: Int(options.getHTTPProxyServerPort()))
proxySettings.httpServer = proxyServer proxySettings.httpServer = proxyServer
proxySettings.httpsServer = proxyServer proxySettings.httpsServer = proxyServer
if await SharedPreferences.systemProxyEnabled.get() { if systemProxyEnabled {
proxySettings.httpEnabled = true proxySettings.httpEnabled = true
proxySettings.httpsEnabled = true proxySettings.httpsEnabled = true
} }
@@ -194,7 +197,7 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
if tunFdFromLoop != -1 { if tunFdFromLoop != -1 {
ret0_.pointee = tunFdFromLoop ret0_.pointee = tunFdFromLoop
} else { } else {
throw NSError(domain: "missing file descriptor", code: 0) throw NSError(domain: "ExtensionPlatformInterface", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Missing file descriptor")])
} }
} }
@@ -204,16 +207,29 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
public func autoDetectControl(_: Int32) throws {} public func autoDetectControl(_: Int32) throws {}
public func findConnectionOwner(_: Int32, sourceAddress _: String?, sourcePort _: Int32, destinationAddress _: String?, destinationPort _: Int32, ret0_ _: UnsafeMutablePointer<Int32>?) throws { public func findConnectionOwner(_ ipProtocol: Int32, sourceAddress: String?, sourcePort: Int32, destinationAddress: String?, destinationPort: Int32) throws -> LibboxConnectionOwner {
throw NSError(domain: "not implemented", code: 0) #if os(macOS)
} if Variant.useSystemExtension {
guard let sourceAddress, let destinationAddress else {
public func packageName(byUid _: Int32, error _: NSErrorPointer) -> String { throw NSError(domain: "findConnectionOwner", code: 0, userInfo: [
"" NSLocalizedDescriptionKey: "Missing source or destination address",
} ])
}
public func uid(byPackageName _: String?, ret0_ _: UnsafeMutablePointer<Int32>?) throws { let owner = try RootHelperClient.shared.findConnectionOwner(
throw NSError(domain: "not implemented", code: 0) ipProtocol: ipProtocol,
sourceAddress: sourceAddress,
sourcePort: sourcePort,
destinationAddress: destinationAddress,
destinationPort: destinationPort
)
let result = LibboxConnectionOwner()
result.userId = owner.userId
result.userName = owner.userName
result.processPath = owner.processPath
return result
}
#endif
throw NSError(domain: "ExtensionPlatformInterface", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Not implemented")])
} }
public func useProcFS() -> Bool { public func useProcFS() -> Bool {
@@ -264,7 +280,7 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
public func getInterfaces() throws -> LibboxNetworkInterfaceIteratorProtocol { public func getInterfaces() throws -> LibboxNetworkInterfaceIteratorProtocol {
guard let nwMonitor else { guard let nwMonitor else {
throw NSError(domain: "NWMonitor not started", code: 0) throw NSError(domain: "ExtensionPlatformInterface", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "NWMonitor not started")])
} }
let path = nwMonitor.currentPath let path = nwMonitor.currentPath
if path.status == .unsatisfied { if path.status == .unsatisfied {
@@ -313,10 +329,10 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
} }
public func includeAllNetworks() -> Bool { public func includeAllNetworks() -> Bool {
#if !os(tvOS) #if os(tvOS)
return SharedPreferences.includeAllNetworks.getBlocking()
#else
return false return false
#else
return tunnel.overridePreferences?.includeAllNetworks ?? false
#endif #endif
} }
@@ -324,12 +340,20 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
guard let networkSettings else { guard let networkSettings else {
return return
} }
tunnel.reasserting = true runBlocking {
tunnel.setTunnelNetworkSettings(nil) { _ in self.tunnel.reasserting = true
defer { self.tunnel.reasserting = false }
await withCheckedContinuation { continuation in
self.tunnel.setTunnelNetworkSettings(nil) { _ in
continuation.resume()
}
}
await withCheckedContinuation { continuation in
self.tunnel.setTunnelNetworkSettings(networkSettings) { _ in
continuation.resume()
}
}
} }
tunnel.setTunnelNetworkSettings(networkSettings) { _ in
}
tunnel.reasserting = false
} }
public func readWIFIState() -> LibboxWIFIState? { public func readWIFIState() -> LibboxWIFIState? {
@@ -342,6 +366,9 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
} }
return LibboxWIFIState(network.ssid, wifiBSSID: network.bssid)! return LibboxWIFIState(network.ssid, wifiBSSID: network.bssid)!
#elseif os(macOS) #elseif os(macOS)
if Variant.useSystemExtension {
return UserServiceClient.shared.readWIFIState()
}
guard let interface = CWWiFiClient.shared().interface() else { guard let interface = CWWiFiClient.shared().interface() else {
return nil return nil
} }
@@ -425,6 +452,8 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
func reset() { func reset() {
networkSettings = nil networkSettings = nil
nwMonitor?.cancel()
nwMonitor = nil
} }
public func send(_ notification: LibboxNotification?) throws { public func send(_ notification: LibboxNotification?) throws {
@@ -432,6 +461,12 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
guard let notification else { guard let notification else {
return return
} }
#if os(macOS)
if Variant.useSystemExtension {
try UserServiceClient.shared.sendNotification(notification)
return
}
#endif
let center = UNUserNotificationCenter.current() let center = UNUserNotificationCenter.current()
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
+78 -29
View File
@@ -1,6 +1,9 @@
import Foundation import Foundation
import Libbox import Libbox
import NetworkExtension import NetworkExtension
import os
private let logger = Logger(category: "ExtensionProfile")
@MainActor @MainActor
public class ExtensionProfile: ObservableObject { public class ExtensionProfile: ObservableObject {
@@ -24,17 +27,19 @@ public class ExtensionProfile: ObservableObject {
observer = NotificationCenter.default.addObserver( observer = NotificationCenter.default.addObserver(
forName: NSNotification.Name.NEVPNStatusDidChange, forName: NSNotification.Name.NEVPNStatusDidChange,
object: manager.connection, object: manager.connection,
queue: .main queue: nil
) { [weak self] notification in ) { [weak self] notification in
guard let self else {
return
}
guard let connection = notification.object as? NEVPNConnection else { guard let connection = notification.object as? NEVPNConnection else {
return return
} }
self.connection = connection Task { @MainActor in
self.status = connection.status guard let self else {
self.connectedDate = connection.connectedDate return
}
self.connection = connection
self.status = connection.status
self.connectedDate = connection.connectedDate
}
} }
} }
@@ -78,7 +83,7 @@ public class ExtensionProfile: ObservableObject {
} }
public func start() async throws { public func start() async throws {
await fetchProfile() try await fetchProfile()
manager.isEnabled = true manager.isEnabled = true
let alwaysOn = await SharedPreferences.alwaysOn.get() let alwaysOn = await SharedPreferences.alwaysOn.get()
let onDemandEnabled = await SharedPreferences.onDemandEnabled.get() let onDemandEnabled = await SharedPreferences.onDemandEnabled.get()
@@ -96,29 +101,73 @@ public class ExtensionProfile: ObservableObject {
} }
#endif #endif
try await manager.saveToPreferences() try await manager.saveToPreferences()
#if os(macOS) let options = try await prepareStartOptions()
if Variant.useSystemExtension { try manager.connection.startVPNTunnel(options: options)
try manager.connection.startVPNTunnel(options: [
"username": NSString(string: NSUserName()),
"manualStart": NSNumber(value: true),
])
return
}
#endif
try manager.connection.startVPNTunnel(options: [
"manualStart": NSNumber(value: true),
])
} }
public func fetchProfile() async { public func reloadService() async throws {
do { let options = try await prepareStartOptions()
if let profile = try await ProfileManager.get(Int64(SharedPreferences.selectedProfileID.get())) { let data = try ExtensionStartOptions.encode(options)
if profile.type == .icloud { guard let session = connection as? NETunnelProviderSession else {
_ = try profile.read() throw NSError(domain: "ExtensionStartOptions", code: -1, userInfo: [
NSLocalizedDescriptionKey: "Tunnel session unavailable",
])
}
let response = try await withCheckedThrowingContinuation { continuation in
do {
try session.sendProviderMessage(data) { response in
continuation.resume(returning: response)
} }
} catch {
continuation.resume(throwing: error)
}
}
if let response, !response.isEmpty {
let message = String(data: response, encoding: .utf8) ?? "Unknown error"
throw NSError(domain: "ExtensionStartOptions", code: -1, userInfo: [
NSLocalizedDescriptionKey: message,
])
}
}
private func prepareStartOptions() async throws -> [String: NSObject] {
var options: [String: NSObject] = [
"manualStart": NSNumber(value: true),
]
let profileID = await SharedPreferences.selectedProfileID.get()
guard let profile = try await ProfileManager.get(profileID) else {
throw NSError(domain: "ExtensionProfile", code: -1, userInfo: [
NSLocalizedDescriptionKey: "Missing selected profile",
])
}
let configContent = try profile.read()
options["configContent"] = NSString(string: configContent)
options["ignoreMemoryLimit"] = await NSNumber(value: SharedPreferences.ignoreMemoryLimit.get())
options["systemProxyEnabled"] = await NSNumber(value: SharedPreferences.systemProxyEnabled.get())
options["excludeDefaultRoute"] = await NSNumber(value: SharedPreferences.excludeDefaultRoute.get())
options["autoRouteUseSubRangesByDefault"] = await NSNumber(value: SharedPreferences.autoRouteUseSubRangesByDefault.get())
options["excludeAPNsRoute"] = await NSNumber(value: SharedPreferences.excludeAPNsRoute.get())
#if !os(tvOS)
options["includeAllNetworks"] = await NSNumber(value: SharedPreferences.includeAllNetworks.get())
#endif
#if os(tvOS)
options["commandServerPort"] = await NSNumber(value: SharedPreferences.commandServerPort.get())
options["commandServerSecret"] = await NSString(string: SharedPreferences.commandServerSecret.get())
#endif
return options
}
public func fetchProfile() async throws {
if let profile = try await ProfileManager.get(Int64(SharedPreferences.selectedProfileID.get())) {
if profile.type == .icloud {
_ = try profile.read()
} }
} catch {
NSLog("fetchProfile error: \(error.localizedDescription)")
} }
} }
@@ -130,7 +179,7 @@ public class ExtensionProfile: ObservableObject {
do { do {
try LibboxNewStandaloneCommandClient()!.serviceClose() try LibboxNewStandaloneCommandClient()!.serviceClose()
} catch { } catch {
NSLog("serviceClose error: \(error.localizedDescription)") logger.debug("serviceClose error: \(error.localizedDescription)")
} }
manager.connection.stopVPNTunnel() manager.connection.stopVPNTunnel()
} }
@@ -142,7 +191,7 @@ public class ExtensionProfile: ObservableObject {
try await Task.sleep(nanoseconds: NSEC_PER_SEC) try await Task.sleep(nanoseconds: NSEC_PER_SEC)
waitSeconds += 1 waitSeconds += 1
if waitSeconds >= 5 { if waitSeconds >= 5 {
throw NSError(domain: "Restart service timeout", code: 0) throw NSError(domain: "ExtensionProfile", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Restart service timeout")])
} }
} }
try await start() try await start()
+147 -50
View File
@@ -1,6 +1,7 @@
import Foundation import Foundation
import Libbox import Libbox
import NetworkExtension import NetworkExtension
import os.log
#if os(iOS) #if os(iOS)
import WidgetKit import WidgetKit
#endif #endif
@@ -9,20 +10,108 @@ import NetworkExtension
#endif #endif
open class ExtensionProvider: NEPacketTunnelProvider { open class ExtensionProvider: NEPacketTunnelProvider {
public var username: String? private static let logger = Logger(category: "ExtensionProvider")
private var commandServer: LibboxCommandServer!
public private(set) var commandServer: LibboxCommandServer?
private var platformInterface: ExtensionPlatformInterface! private var platformInterface: ExtensionPlatformInterface!
public var tunnelOptions: [String: NSObject]?
private var startOptionsURL: URL?
public struct OverridePreferences {
public var includeAllNetworks: Bool = false
public var systemProxyEnabled: Bool = true
public var excludeDefaultRoute: Bool = false
public var autoRouteUseSubRangesByDefault: Bool = false
public var excludeAPNsRoute: Bool = false
}
public var overridePreferences: OverridePreferences?
private func applyStartOptions(_ options: [String: NSObject]) {
tunnelOptions = options
var prefs = OverridePreferences()
prefs.includeAllNetworks = (options["includeAllNetworks"] as? NSNumber)?.boolValue ?? false
prefs.systemProxyEnabled = (options["systemProxyEnabled"] as? NSNumber)?.boolValue ?? true
prefs.excludeDefaultRoute = (options["excludeDefaultRoute"] as? NSNumber)?.boolValue ?? false
prefs.autoRouteUseSubRangesByDefault = (options["autoRouteUseSubRangesByDefault"] as? NSNumber)?.boolValue ?? false
prefs.excludeAPNsRoute = (options["excludeAPNsRoute"] as? NSNumber)?.boolValue ?? false
overridePreferences = prefs
}
private func persistStartOptions(_ options: [String: NSObject]) throws {
guard let startOptionsURL else {
return
}
let data = try ExtensionStartOptions.encode(options)
try data.write(to: startOptionsURL, options: .atomic)
}
#if os(macOS)
private var xpcListener: NSXPCListener?
private var xpcService: CommandXPCService?
private var locationManager: CLLocationManager?
private var locationDelegate: stubLocationDelegate?
#endif
override open func startTunnel(options startOptions: [String: NSObject]?) async throws {
let basePath: String
let workingPath: String
let tempPath: String
#if os(macOS)
if Variant.useSystemExtension {
let containerURL = FileManager.default.homeDirectoryForCurrentUser
basePath = containerURL.path
workingPath = containerURL.appendingPathComponent("Working").path
tempPath = containerURL.appendingPathComponent("Temp").path
} else {
basePath = FilePath.sharedDirectory.relativePath
workingPath = FilePath.workingDirectory.relativePath
tempPath = FilePath.cacheDirectory.relativePath
}
#else
basePath = FilePath.sharedDirectory.relativePath
workingPath = FilePath.workingDirectory.relativePath
tempPath = FilePath.cacheDirectory.relativePath
#endif
startOptionsURL = URL(fileURLWithPath: basePath).appendingPathComponent(ExtensionStartOptions.snapshotFileName)
var effectiveOptions = startOptions
if let startOptions {
do {
try persistStartOptions(startOptions)
} catch {
throw ExtensionStartupError("(packet-tunnel) error: persist start options: \(error.localizedDescription)")
}
} else if let startOptionsURL, FileManager.default.fileExists(atPath: startOptionsURL.path) {
do {
let data = try Data(contentsOf: startOptionsURL)
effectiveOptions = try ExtensionStartOptions.decode(data)
} catch {
throw ExtensionStartupError("(packet-tunnel) error: load start options: \(error.localizedDescription)")
}
} else {
throw ExtensionStartupError("(packet-tunnel) error: missing start options")
}
if let effectiveOptions {
applyStartOptions(effectiveOptions)
}
override open func startTunnel(options _: [String: NSObject]?) async throws {
let options = LibboxSetupOptions() let options = LibboxSetupOptions()
options.basePath = FilePath.sharedDirectory.relativePath options.basePath = basePath
options.workingPath = FilePath.workingDirectory.relativePath options.workingPath = workingPath
options.tempPath = FilePath.cacheDirectory.relativePath options.tempPath = tempPath
options.logMaxLines = 3000 options.logMaxLines = 3000
#if os(tvOS) #if os(tvOS)
options.commandServerListenPort = await SharedPreferences.commandServerPort.get() if let port = effectiveOptions?["commandServerPort"] as? NSNumber {
options.commandServerSecret = await SharedPreferences.commandServerSecret.get() options.commandServerListenPort = port.int32Value
}
if let secret = effectiveOptions?["commandServerSecret"] as? String {
options.commandServerSecret = secret
}
#endif #endif
var setupError: NSError? var setupError: NSError?
@@ -31,13 +120,8 @@ open class ExtensionProvider: NEPacketTunnelProvider {
throw ExtensionStartupError("(packet-tunnel) error: setup service: \(setupError.localizedDescription)") throw ExtensionStartupError("(packet-tunnel) error: setup service: \(setupError.localizedDescription)")
} }
var stderrError: NSError? let ignoreMemoryLimit = (effectiveOptions?["ignoreMemoryLimit"] as? NSNumber)?.boolValue ?? false
LibboxRedirectStderr(FilePath.cacheDirectory.appendingPathComponent("stderr.log").relativePath, &stderrError) LibboxSetMemoryLimit(!ignoreMemoryLimit)
if let stderrError {
throw ExtensionStartupError("(packet-tunnel) redirect stderr error: \(stderrError.localizedDescription)")
}
await LibboxSetMemoryLimit(!SharedPreferences.ignoreMemoryLimit.get())
if platformInterface == nil { if platformInterface == nil {
platformInterface = ExtensionPlatformInterface(self) platformInterface = ExtensionPlatformInterface(self)
@@ -48,12 +132,31 @@ open class ExtensionProvider: NEPacketTunnelProvider {
throw ExtensionStartupError("(packet-tunnel): create command server error: \(error.localizedDescription)") throw ExtensionStartupError("(packet-tunnel): create command server error: \(error.localizedDescription)")
} }
do { do {
try commandServer.start() try commandServer!.start()
} catch { } catch {
throw ExtensionStartupError("(packet-tunnel): start command server error: \(error.localizedDescription)") throw ExtensionStartupError("(packet-tunnel): start command server error: \(error.localizedDescription)")
} }
#if os(macOS)
if Variant.useSystemExtension {
let socketPath = options.basePath + "/command.sock"
xpcService = CommandXPCService(socketPath: socketPath)
let machServiceName = AppConfiguration.appGroupID + ".system"
xpcListener = NSXPCListener(machServiceName: machServiceName)
xpcListener!.delegate = xpcService
xpcListener!.resume()
Self.logger.info("set Command Server")
xpcService!.commandServer = commandServer
}
#endif
writeMessage("(packet-tunnel): Here I stand") writeMessage("(packet-tunnel): Here I stand")
try await startService() try await startService()
#if os(macOS)
if Variant.useSystemExtension {
xpcService!.markServiceReady()
}
#endif
#if os(iOS) #if os(iOS)
if #available(iOS 18.0, *) { if #available(iOS 18.0, *) {
ControlCenter.shared.reloadControls(ofKind: ExtensionProfile.controlKind) ControlCenter.shared.reloadControls(ofKind: ExtensionProfile.controlKind)
@@ -68,48 +171,28 @@ open class ExtensionProvider: NEPacketTunnelProvider {
} }
private func startService() async throws { private func startService() async throws {
let profileID = await SharedPreferences.selectedProfileID.get() guard let configContent = tunnelOptions?["configContent"] as? String else {
let profile: Profile? throw ExtensionStartupError("(packet-tunnel) error: missing configContent in tunnel options")
do {
profile = try await ProfileManager.get(profileID)
} catch {
throw ExtensionStartupError("(packet-tunnel) error: read selected profile: \(error.localizedDescription)")
}
guard let profile else {
throw ExtensionStartupError("(packet-tunnel) error: missing selected profile")
}
let configContent: String
do {
configContent = try profile.read()
} catch {
throw ExtensionStartupError("(packet-tunnel) error: read config file \(profile.path): \(error.localizedDescription)")
} }
let options = LibboxOverrideOptions() let options = LibboxOverrideOptions()
do { do {
try commandServer.startOrReloadService(configContent, options: options) try commandServer!.startOrReloadService(configContent, options: options)
} catch { } catch {
throw ExtensionStartupError("(packet-tunnel) error: start service: \(error.localizedDescription)") throw ExtensionStartupError("(packet-tunnel) error: start service: \(error.localizedDescription)")
} }
#if os(macOS) #if os(macOS)
await SharedPreferences.startedByUser.set(true) if !Variant.useSystemExtension, commandServer!.needWIFIState() {
if commandServer.needWIFIState() { locationManager = CLLocationManager()
if !Variant.useSystemExtension { locationDelegate = stubLocationDelegate()
locationManager = CLLocationManager() locationManager?.delegate = locationDelegate
locationDelegate = stubLocationDelegate() locationManager?.requestLocation()
locationManager?.delegate = locationDelegate
locationManager?.requestLocation()
} else {
writeMessage("(packet-tunnel) WIFI SSID and BSSID information is not currently available in the standalone version of SFM. We are working on resolving this issue.")
}
} }
#endif #endif
} }
#if os(macOS) #if os(macOS)
private var locationManager: CLLocationManager?
private var locationDelegate: stubLocationDelegate?
class stubLocationDelegate: NSObject, CLLocationManagerDelegate { class stubLocationDelegate: NSObject, CLLocationManagerDelegate {
func locationManagerDidChangeAuthorization(_: CLLocationManager) {} func locationManagerDidChangeAuthorization(_: CLLocationManager) {}
@@ -122,9 +205,9 @@ open class ExtensionProvider: NEPacketTunnelProvider {
func stopService() { func stopService() {
do { do {
try commandServer.closeService() try commandServer?.closeService()
} catch { } catch {
writeMessage("(packet-tunnel) error: stop service: \(error.localizedDescription)") writeMessage("(packet-tunnel) stop service: \(error.localizedDescription)")
} }
if let platformInterface { if let platformInterface {
platformInterface.reset() platformInterface.reset()
@@ -149,9 +232,15 @@ open class ExtensionProvider: NEPacketTunnelProvider {
commandServer = nil commandServer = nil
} }
#if os(macOS) #if os(macOS)
if reason == .userInitiated { if Variant.useSystemExtension {
await SharedPreferences.startedByUser.set(reason == .userInitiated) xpcListener?.invalidate()
xpcListener = nil
xpcService?.commandServer = nil
xpcService = nil
UserServiceEndpointRegistry.shared.clear()
} }
locationManager = nil
locationDelegate = nil
#endif #endif
#if os(iOS) #if os(iOS)
if #available(iOS 18.0, *) { if #available(iOS 18.0, *) {
@@ -161,7 +250,15 @@ open class ExtensionProvider: NEPacketTunnelProvider {
} }
override open func handleAppMessage(_ messageData: Data) async -> Data? { override open func handleAppMessage(_ messageData: Data) async -> Data? {
messageData do {
let options = try ExtensionStartOptions.decode(messageData)
applyStartOptions(options)
try persistStartOptions(options)
try await reloadService()
return nil
} catch {
return error.localizedDescription.data(using: .utf8)
}
} }
override open func sleep() async { override open func sleep() async {
@@ -0,0 +1,19 @@
import Foundation
enum ExtensionStartOptions {
static let snapshotFileName = "start_options.plist"
static func encode(_ options: [String: NSObject]) throws -> Data {
try PropertyListSerialization.data(fromPropertyList: options, format: .binary, options: 0)
}
static func decode(_ data: Data) throws -> [String: NSObject] {
let plist = try PropertyListSerialization.propertyList(from: data, options: [], format: nil)
guard let options = plist as? [String: NSObject] else {
throw NSError(domain: "ExtensionStartOptions", code: -1, userInfo: [
NSLocalizedDescriptionKey: "Invalid start options payload",
])
}
return options
}
}
@@ -0,0 +1,25 @@
#if os(macOS)
import Foundation
import ServiceManagement
public enum HelperServiceManager {
private static var rootHelperService: SMAppService {
SMAppService.daemon(plistName: "\(AppConfiguration.rootHelperBundleID).plist")
}
public static var rootHelperStatus: SMAppService.Status {
rootHelperService.status
}
public static func registerRootHelper() throws {
if rootHelperService.status == .enabled {
try rootHelperService.unregister()
}
try rootHelperService.register()
}
public static func unregisterRootHelper() throws {
try rootHelperService.unregister()
}
}
#endif
+221
View File
@@ -0,0 +1,221 @@
#if os(macOS)
import Foundation
import os
private let logger = Logger(category: "RootHelperXPC")
@objc public class ConnectionOwnerResult: NSObject, NSSecureCoding {
public static let supportsSecureCoding = true
@objc public var userId: Int32
@objc public var userName: String
@objc public var processPath: String
public init(userId: Int32, userName: String, processPath: String) {
self.userId = userId
self.userName = userName
self.processPath = processPath
}
public required init?(coder: NSCoder) {
userId = coder.decodeInt32(forKey: "userId")
userName = coder.decodeObject(of: NSString.self, forKey: "userName") as? String ?? ""
processPath = coder.decodeObject(of: NSString.self, forKey: "processPath") as? String ?? ""
}
public func encode(with coder: NSCoder) {
coder.encode(userId, forKey: "userId")
coder.encode(userName as NSString, forKey: "userName")
coder.encode(processPath as NSString, forKey: "processPath")
}
}
@objc public protocol RootHelperProtocol {
func findConnectionOwner(
ipProtocol: Int32,
sourceAddress: String,
sourcePort: Int32,
destinationAddress: String,
destinationPort: Int32,
reply: @escaping (ConnectionOwnerResult?, NSError?) -> Void
)
func getWorkingDirectorySize(reply: @escaping (Int64, NSError?) -> Void)
func cleanWorkingDirectory(reply: @escaping (NSError?) -> Void)
}
public enum RootHelperXPC {
public static func configureInterface(_ interface: NSXPCInterface) {
let resultClasses = NSSet(array: [ConnectionOwnerResult.self, NSString.self]) as! Set<AnyHashable>
interface.setClasses(
resultClasses,
for: #selector(RootHelperProtocol.findConnectionOwner(ipProtocol:sourceAddress:sourcePort:destinationAddress:destinationPort:reply:)),
argumentIndex: 0,
ofReply: true
)
}
}
public class RootHelperClient {
public static let shared = RootHelperClient()
private var connection: NSXPCConnection?
private let connectionLock = NSLock()
private init() {}
private func getConnection() -> NSXPCConnection {
connectionLock.lock()
defer { connectionLock.unlock() }
if let existing = connection {
return existing
}
let newConnection = NSXPCConnection(machServiceName: AppConfiguration.rootHelperMachService)
let remoteInterface = NSXPCInterface(with: RootHelperProtocol.self)
RootHelperXPC.configureInterface(remoteInterface)
newConnection.remoteObjectInterface = remoteInterface
newConnection.invalidationHandler = { [weak self] in
guard let self else { return }
connectionLock.lock()
connection = nil
connectionLock.unlock()
}
newConnection.resume()
connection = newConnection
return newConnection
}
private func performXPCCall<T>(
_ operation: String,
call: (RootHelperProtocol, @escaping (T?, NSError?) -> Void) -> Void
) throws -> T {
let semaphore = DispatchSemaphore(value: 0)
var result: T?
var resultError: NSError?
let conn = getConnection()
guard let proxy = conn.remoteObjectProxyWithErrorHandler({ error in
logger.error("\(operation) XPC error: \(error.localizedDescription)")
resultError = error as NSError
semaphore.signal()
}) as? RootHelperProtocol else {
connectionLock.lock()
connection = nil
connectionLock.unlock()
conn.invalidate()
throw NSError(domain: "RootHelper", code: -1, userInfo: [
NSLocalizedDescriptionKey: "Failed to get RootHelper proxy",
])
}
call(proxy) { value, error in
result = value
resultError = error
semaphore.signal()
}
let timeout = DispatchTime.now() + .seconds(5)
if semaphore.wait(timeout: timeout) == .timedOut {
let error = NSError(domain: "RootHelper", code: -1, userInfo: [
NSLocalizedDescriptionKey: "\(operation) request timeout",
])
logger.error("\(operation): timeout")
throw error
}
if let error = resultError {
logger.error("\(operation) error: \(error.localizedDescription)")
throw error
}
guard let value = result else {
let error = NSError(domain: "RootHelper", code: -1, userInfo: [
NSLocalizedDescriptionKey: "\(operation) returned nil",
])
throw error
}
return value
}
private func performXPCCallVoid(
_ operation: String,
call: (RootHelperProtocol, @escaping (NSError?) -> Void) -> Void
) throws {
let semaphore = DispatchSemaphore(value: 0)
var resultError: NSError?
let conn = getConnection()
guard let proxy = conn.remoteObjectProxyWithErrorHandler({ error in
logger.error("\(operation) XPC error: \(error.localizedDescription)")
resultError = error as NSError
semaphore.signal()
}) as? RootHelperProtocol else {
connectionLock.lock()
connection = nil
connectionLock.unlock()
conn.invalidate()
throw NSError(domain: "RootHelper", code: -1, userInfo: [
NSLocalizedDescriptionKey: "Failed to get RootHelper proxy",
])
}
call(proxy) { error in
resultError = error
semaphore.signal()
}
let timeout = DispatchTime.now() + .seconds(5)
if semaphore.wait(timeout: timeout) == .timedOut {
let error = NSError(domain: "RootHelper", code: -1, userInfo: [
NSLocalizedDescriptionKey: "\(operation) request timeout",
])
logger.error("\(operation): timeout")
throw error
}
if let error = resultError {
logger.error("\(operation) error: \(error.localizedDescription)")
throw error
}
}
public func findConnectionOwner(
ipProtocol: Int32,
sourceAddress: String,
sourcePort: Int32,
destinationAddress: String,
destinationPort: Int32
) throws -> ConnectionOwnerResult {
try performXPCCall("findConnectionOwner") { proxy, reply in
proxy.findConnectionOwner(
ipProtocol: ipProtocol,
sourceAddress: sourceAddress,
sourcePort: sourcePort,
destinationAddress: destinationAddress,
destinationPort: destinationPort,
reply: reply
)
}
}
public func getWorkingDirectorySize() throws -> Int64 {
try performXPCCall("getWorkingDirectorySize") { proxy, reply in
proxy.getWorkingDirectorySize { size, error in
reply(size as Int64?, error)
}
}
}
public func cleanWorkingDirectory() throws {
try performXPCCallVoid("cleanWorkingDirectory") { proxy, reply in
proxy.cleanWorkingDirectory(reply: reply)
}
}
}
#endif
+5 -2
View File
@@ -1,7 +1,10 @@
#if os(macOS) #if os(macOS)
import Foundation import Foundation
import os
import SystemExtensions import SystemExtensions
private let logger = Logger(category: "SystemExtension")
public class SystemExtension: NSObject, OSSystemExtensionRequestDelegate { public class SystemExtension: NSObject, OSSystemExtensionRequestDelegate {
private let forceUpdate: Bool private let forceUpdate: Bool
private let inBackground: Bool private let inBackground: Bool
@@ -26,10 +29,10 @@
existing.bundleVersion == ext.bundleVersion, existing.bundleVersion == ext.bundleVersion,
existing.bundleShortVersion == ext.bundleShortVersion existing.bundleShortVersion == ext.bundleShortVersion
{ {
NSLog("Skip update system extension") logger.info("Skip update system extension")
return .cancel return .cancel
} else { } else {
NSLog("Update system extension") logger.info("Update system extension")
return .replace return .replace
} }
} }
@@ -0,0 +1,204 @@
#if os(macOS)
import CoreWLAN
import Dispatch
import Foundation
import os
import UserNotifications
private let logger = Logger(category: "UserService")
public extension Notification.Name {
static let extensionRequiresWIFIState = Notification.Name("extensionRequiresWIFIState")
static let extensionRequiresHelperService = Notification.Name("extensionRequiresHelperService")
static let navigateToSettingsPage = Notification.Name("navigateToSettingsPage")
}
public final class UserServiceEndpointPublisher: NSObject, NSXPCListenerDelegate {
public static let shared = UserServiceEndpointPublisher()
private var listener: NSXPCListener?
private let exportedObject = UserServiceHandler()
public func start() {
guard listener == nil else {
return
}
let listener = NSXPCListener.anonymous()
listener.delegate = self
listener.resume()
self.listener = listener
registerEndpoint(listener.endpoint)
}
public func stop() {
if let listener {
listener.invalidate()
self.listener = nil
}
registerEndpoint(nil)
}
public func refreshEndpointRegistration() {
guard let listener else {
return
}
registerEndpoint(listener.endpoint)
}
public func listener(_: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
let allowedBundleIDs = [AppConfiguration.systemExtensionBundleID]
guard XPCConnectionValidator.validateConnection(
newConnection,
teamID: AppConfiguration.teamID,
allowedBundleIDs: allowedBundleIDs
) else {
let info = XPCConnectionValidator.getConnectionInfo(newConnection)
logger.warning("Rejected XPC connection: pid=\(info.pid), bundleID=\(info.bundleID ?? "unknown"), teamID=\(info.teamID ?? "unknown")")
return false
}
newConnection.exportedInterface = NSXPCInterface(with: UserServiceProtocol.self)
newConnection.exportedObject = exportedObject
newConnection.resume()
return true
}
public func checkExtensionRequirements() {
Task.detached {
let machServiceName = AppConfiguration.appGroupID + ".system"
let connection = NSXPCConnection(machServiceName: machServiceName)
let remoteInterface = NSXPCInterface(with: CommandXPCProtocol.self)
CommandXPC.configureInterface(remoteInterface)
connection.remoteObjectInterface = remoteInterface
connection.resume()
guard let proxy = connection.remoteObjectProxyWithErrorHandler({ error in
logger.error("Extension requirements check error: \(error.localizedDescription)")
connection.invalidate()
}) as? CommandXPCProtocol else {
connection.invalidate()
return
}
proxy.extensionRequirements { needWIFI, needProcess, error in
if let error {
logger.error("Extension requirements error: \(error.localizedDescription)")
connection.invalidate()
return
}
if needWIFI {
Task { @MainActor in
NotificationCenter.default.post(name: .extensionRequiresWIFIState, object: nil)
}
}
if needProcess {
Task { @MainActor in
NotificationCenter.default.post(name: .extensionRequiresHelperService, object: nil)
}
}
connection.invalidate()
}
}
}
private func registerEndpoint(_ endpoint: NSXPCListenerEndpoint?) {
let machServiceName = AppConfiguration.appGroupID + ".system"
let connection = NSXPCConnection(machServiceName: machServiceName)
let remoteInterface = NSXPCInterface(with: CommandXPCProtocol.self)
CommandXPC.configureInterface(remoteInterface)
connection.remoteObjectInterface = remoteInterface
connection.resume()
guard let proxy = connection.remoteObjectProxyWithErrorHandler({ error in
logger.error("UserService registration error: \(error.localizedDescription)")
connection.invalidate()
}) as? CommandXPCProtocol else {
connection.invalidate()
return
}
proxy.registerUserServiceEndpoint(endpoint) { error in
if let error {
logger.error("UserService register failed: \(error.localizedDescription)")
}
connection.invalidate()
}
}
}
private final class UserServiceHandler: NSObject, UserServiceProtocol {
func getWIFIState(reply: @escaping (String?, String?, NSError?) -> Void) {
let client = CWWiFiClient.shared()
guard let interface = client.interface() else {
reply(nil, nil, nil)
return
}
let ssid = interface.ssid()
let bssid = interface.bssid()
reply(ssid, bssid, nil)
}
func sendNotification(
identifier: String,
typeName _: String,
typeID _: Int32,
title: String,
subtitle: String,
body: String,
openURL: String,
reply: @escaping (NSError?) -> Void
) {
Task {
do {
let center = UNUserNotificationCenter.current()
let settings = await center.notificationSettings()
if settings.authorizationStatus == .notDetermined {
let granted = try await center.requestAuthorization(options: [.alert, .sound])
if !granted {
let error = NSError(domain: "UserService", code: -1, userInfo: [
NSLocalizedDescriptionKey: "Notification permission denied",
])
logger.error("sendNotification error: \(error.localizedDescription)")
reply(error)
return
}
} else if settings.authorizationStatus == .denied {
let error = NSError(domain: "UserService", code: -1, userInfo: [
NSLocalizedDescriptionKey: "Notification permission denied",
])
logger.error("sendNotification error: \(error.localizedDescription)")
reply(error)
return
}
let content = UNMutableNotificationContent()
content.title = title
if !subtitle.isEmpty {
content.subtitle = subtitle
}
content.body = body
content.sound = .default
if !openURL.isEmpty {
content.userInfo["openURL"] = openURL
}
let request = UNNotificationRequest(
identifier: identifier,
content: content,
trigger: nil
)
try await center.add(request)
reply(nil)
} catch {
let nsError = error as NSError
logger.error("sendNotification error: \(nsError.localizedDescription)")
reply(nsError)
}
}
}
}
#endif
@@ -0,0 +1,28 @@
#if os(macOS)
import Foundation
final class UserServiceEndpointRegistry {
static let shared = UserServiceEndpointRegistry()
private let lock = NSLock()
private var endpoint: NSXPCListenerEndpoint?
func update(_ endpoint: NSXPCListenerEndpoint) {
lock.lock()
self.endpoint = endpoint
lock.unlock()
}
func clear() {
lock.lock()
endpoint = nil
lock.unlock()
}
func get() -> NSXPCListenerEndpoint? {
lock.lock()
defer { lock.unlock() }
return endpoint
}
}
#endif
+159
View File
@@ -0,0 +1,159 @@
#if os(macOS)
import Foundation
import Libbox
import os
private let logger = Logger(category: "UserServiceXPC")
@objc public protocol UserServiceProtocol {
func getWIFIState(reply: @escaping (String?, String?, NSError?) -> Void)
func sendNotification(
identifier: String,
typeName: String,
typeID: Int32,
title: String,
subtitle: String,
body: String,
openURL: String,
reply: @escaping (NSError?) -> Void
)
}
public class UserServiceClient {
public static let shared = UserServiceClient()
private var connection: NSXPCConnection?
private let connectionLock = NSLock()
private init() {}
private func getConnection() -> NSXPCConnection? {
connectionLock.lock()
defer { connectionLock.unlock() }
if let existing = connection {
return existing
}
guard let endpoint = UserServiceEndpointRegistry.shared.get() else {
logger.error("UserService endpoint unavailable")
return nil
}
let newConnection = NSXPCConnection(listenerEndpoint: endpoint)
newConnection.remoteObjectInterface = NSXPCInterface(with: UserServiceProtocol.self)
newConnection.invalidationHandler = { [weak self] in
guard let self else { return }
connectionLock.lock()
connection = nil
connectionLock.unlock()
}
newConnection.resume()
connection = newConnection
return newConnection
}
private func getProxy() -> UserServiceProtocol? {
guard let conn = getConnection() else {
return nil
}
guard let proxy = conn.remoteObjectProxyWithErrorHandler { [weak self] error in
guard let self else { return }
logger.error("UserService XPC error: \(error.localizedDescription)")
connectionLock.lock()
connection = nil
connectionLock.unlock()
} as? UserServiceProtocol else {
connectionLock.lock()
connection = nil
connectionLock.unlock()
conn.invalidate()
return nil
}
return proxy
}
private func performXPCCallVoid(
_ operation: String,
call: (UserServiceProtocol, @escaping (NSError?) -> Void) -> Void
) throws {
let semaphore = DispatchSemaphore(value: 0)
var resultError: NSError?
guard let proxy = getProxy() else {
throw NSError(domain: "UserService", code: -1, userInfo: [
NSLocalizedDescriptionKey: "UserService connection unavailable",
])
}
call(proxy) { error in
resultError = error
semaphore.signal()
}
let deadline = DispatchTime.now() + .seconds(5)
if semaphore.wait(timeout: deadline) == .timedOut {
let error = NSError(domain: "UserService", code: -1, userInfo: [
NSLocalizedDescriptionKey: "\(operation) request timeout",
])
logger.error("\(operation): timeout")
throw error
}
if let error = resultError {
logger.error("\(operation) error: \(error.localizedDescription)")
throw error
}
}
public func readWIFIState() -> LibboxWIFIState? {
let semaphore = DispatchSemaphore(value: 0)
var resultSSID: String?
var resultBSSID: String?
guard let proxy = getProxy() else {
logger.error("readWIFIState: no UserService connection")
return nil
}
proxy.getWIFIState { ssid, bssid, error in
if let error {
logger.error("readWIFIState error: \(error.localizedDescription)")
} else {
resultSSID = ssid
resultBSSID = bssid
}
semaphore.signal()
}
let timeout = DispatchTime.now() + .seconds(5)
if semaphore.wait(timeout: timeout) == .timedOut {
logger.error("readWIFIState: timeout")
return nil
}
guard let ssid = resultSSID, let bssid = resultBSSID else {
return nil
}
return LibboxWIFIState(ssid, wifiBSSID: bssid)
}
public func sendNotification(_ notification: LibboxNotification) throws {
try performXPCCallVoid("sendNotification") { proxy, reply in
proxy.sendNotification(
identifier: notification.identifier,
typeName: notification.typeName,
typeID: notification.typeID,
title: notification.title,
subtitle: notification.subtitle,
body: notification.body,
openURL: notification.openURL,
reply: reply
)
}
}
}
#endif
@@ -0,0 +1,80 @@
#if os(macOS)
import Foundation
import Security
public struct XPCConnectionInfo {
public let pid: pid_t
public let bundleID: String?
public let teamID: String?
}
public enum XPCConnectionValidator {
private static func getSecCode(for connection: NSXPCConnection) -> SecCode? {
let pid = connection.processIdentifier
var code: SecCode?
let attributes = [kSecGuestAttributePid: pid] as CFDictionary
guard SecCodeCopyGuestWithAttributes(nil, attributes, [], &code) == errSecSuccess else {
return nil
}
return code
}
private static func getSigningInfo(_ code: SecCode) -> [String: Any]? {
var staticCode: SecStaticCode?
guard SecCodeCopyStaticCode(code, [], &staticCode) == errSecSuccess,
let staticCode
else {
return nil
}
var info: CFDictionary?
guard SecCodeCopySigningInformation(staticCode, [], &info) == errSecSuccess else {
return nil
}
return info as? [String: Any]
}
public static func getConnectionInfo(_ connection: NSXPCConnection) -> XPCConnectionInfo {
let pid = connection.processIdentifier
guard let secCode = getSecCode(for: connection),
let signingInfo = getSigningInfo(secCode)
else {
return XPCConnectionInfo(pid: pid, bundleID: nil, teamID: nil)
}
let bundleID = signingInfo[kSecCodeInfoIdentifier as String] as? String
let teamID = signingInfo[kSecCodeInfoTeamIdentifier as String] as? String
return XPCConnectionInfo(pid: pid, bundleID: bundleID, teamID: teamID)
}
public static func validateConnection(
_ connection: NSXPCConnection,
teamID: String,
allowedBundleIDs: [String]
) -> Bool {
guard let secCode = getSecCode(for: connection) else {
return false
}
let requirement = "anchor apple generic and certificate leaf[subject.OU] = \"\(teamID)\""
var secRequirement: SecRequirement?
guard SecRequirementCreateWithString(requirement as CFString, [], &secRequirement) == errSecSuccess,
let req = secRequirement,
SecCodeCheckValidity(secCode, [], req) == errSecSuccess
else {
return false
}
guard let signingInfo = getSigningInfo(secCode),
let bundleID = signingInfo[kSecCodeInfoIdentifier as String] as? String,
allowedBundleIDs.contains(bundleID)
else {
return false
}
return true
}
}
#endif
+12
View File
@@ -15,6 +15,13 @@ public enum AppConfiguration {
return value return value
}() }()
public static var teamID: String {
guard let dotIndex = appGroupID.firstIndex(of: ".") else {
fatalError("Invalid appGroupID format: \(appGroupID)")
}
return String(appGroupID[..<dotIndex])
}
public static var extensionBundleID: String { "\(packageName).extension" } public static var extensionBundleID: String { "\(packageName).extension" }
public static var systemExtensionBundleID: String { "\(packageName).system" } public static var systemExtensionBundleID: String { "\(packageName).system" }
public static var fileProviderDomainID: String { "\(packageName).workingdir" } public static var fileProviderDomainID: String { "\(packageName).workingdir" }
@@ -22,4 +29,9 @@ public enum AppConfiguration {
public static var profileUTType: String { "\(packageName).profile" } public static var profileUTType: String { "\(packageName).profile" }
public static var backgroundTaskID: String { "\(packageName).update_profiles" } public static var backgroundTaskID: String { "\(packageName).update_profiles" }
public static var iCloudContainerID: String { "iCloud.\(packageName)" } public static var iCloudContainerID: String { "iCloud.\(packageName)" }
#if os(macOS)
public static var rootHelperBundleID: String { "\(packageName).helper" }
public static var rootHelperMachService: String { "\(appGroupID).helper" }
#endif
} }
+8
View File
@@ -0,0 +1,8 @@
import Foundation
import os
public extension Logger {
init(category: String) {
self.init(subsystem: Bundle.main.bundleIdentifier!, category: category)
}
}
+33
View File
@@ -200,6 +200,9 @@
} }
} }
} }
},
"App Settings" : {
}, },
"Append `0.0.0.0/31` and `::/127` to `route_exclude_address` if not exists." : { "Append `0.0.0.0/31` and `::/127` to `route_exclude_address` if not exists." : {
"localizations" : { "localizations" : {
@@ -979,6 +982,9 @@
} }
} }
} }
},
"Enable" : {
}, },
"enforceRoutes" : { "enforceRoutes" : {
"shouldTranslate" : false "shouldTranslate" : false
@@ -1168,6 +1174,12 @@
} }
} }
} }
},
"Helper Service" : {
},
"Helper Service Required" : {
}, },
"Hide VPN Icon" : { "Hide VPN Icon" : {
"localizations" : { "localizations" : {
@@ -1341,6 +1353,9 @@
} }
} }
} }
},
"In the standalone version of SFM, reading WiFi state requires this app to be running. After you quit the SFM app, the sing-box service cannot continue to provide `wifi_ssid` and `wifi_bssid` routing rules." : {
}, },
"Inbound" : { "Inbound" : {
"localizations" : { "localizations" : {
@@ -1364,6 +1379,9 @@
}, },
"includeAllNetworks" : { "includeAllNetworks" : {
"shouldTranslate" : false "shouldTranslate" : false
},
"Install" : {
}, },
"Install Network Extension" : { "Install Network Extension" : {
"localizations" : { "localizations" : {
@@ -1530,6 +1548,9 @@
} }
} }
} }
},
"Managing working directory requires Helper Service." : {
}, },
"Match Domains" : { "Match Domains" : {
"localizations" : { "localizations" : {
@@ -2428,6 +2449,9 @@
} }
} }
} }
},
"The sing-box service requires Helper Service to provide process lookup functionality, which supports `process_name` and `process_path` routing rules." : {
}, },
"This app needs to be placed under the Applications folder to work." : { "This app needs to be placed under the Applications folder to work." : {
"localizations" : { "localizations" : {
@@ -2438,6 +2462,9 @@
} }
} }
} }
},
"This helper service provides process lookup for `process_name` and `process_path` routing rules, and manages the working directory." : {
}, },
"To Clipboard" : { "To Clipboard" : {
"localizations" : { "localizations" : {
@@ -2508,6 +2535,9 @@
} }
} }
} }
},
"Unavailable" : {
}, },
"Uninstall" : { "Uninstall" : {
"localizations" : { "localizations" : {
@@ -2631,6 +2661,9 @@
} }
} }
} }
},
"WiFi State Access" : {
}, },
"Working Directory" : { "Working Directory" : {
"localizations" : { "localizations" : {
+27 -1
View File
@@ -9,6 +9,8 @@ public struct MainView: View {
@StateObject private var viewModel = MainViewModel() @StateObject private var viewModel = MainViewModel()
@State private var showCardManagement = false @State private var showCardManagement = false
@State private var cardConfigurationVersion = 0 @State private var cardConfigurationVersion = 0
@State private var settingsNavigationPath = NavigationPath()
@State private var pendingSettingsPage: SettingsPage?
private let profileEditor: (Binding<String>, Bool) -> AnyView = { text, isEditable in private let profileEditor: (Binding<String>, Bool) -> AnyView = { text, isEditable in
AnyView(CodeEditTextView(text: text, isEditable: isEditable)) AnyView(CodeEditTextView(text: text, isEditable: isEditable))
@@ -21,11 +23,12 @@ public struct MainView: View {
SidebarView(selection: $viewModel.selection) SidebarView(selection: $viewModel.selection)
.navigationSplitViewColumnWidth(150) .navigationSplitViewColumnWidth(150)
} detail: { } detail: {
NavigationStack { NavigationStack(path: $settingsNavigationPath) {
viewModel.selection.contentView viewModel.selection.contentView
.navigationTitle(viewModel.selection.title) .navigationTitle(viewModel.selection.title)
} }
.environment(\.cardConfigurationVersion, cardConfigurationVersion) .environment(\.cardConfigurationVersion, cardConfigurationVersion)
.environment(\.settingsNavigationPath, $settingsNavigationPath)
.navigationSplitViewColumnWidth(650) .navigationSplitViewColumnWidth(650)
} }
.frame(minHeight: 500) .frame(minHeight: 500)
@@ -33,6 +36,7 @@ public struct MainView: View {
viewModel.onAppear(environments: environments) viewModel.onAppear(environments: environments)
} }
.alert($viewModel.alert) .alert($viewModel.alert)
.globalChecks()
.toolbar { .toolbar {
ToolbarItem(placement: .navigation) { ToolbarItem(placement: .navigation) {
StartStopButton() StartStopButton()
@@ -57,10 +61,32 @@ public struct MainView: View {
} }
.onChangeCompat(of: viewModel.selection) { value in .onChangeCompat(of: viewModel.selection) { value in
viewModel.onSelectionChange(value, environments: environments) viewModel.onSelectionChange(value, environments: environments)
if value != .settings {
settingsNavigationPath = NavigationPath()
pendingSettingsPage = nil
return
}
if let page = pendingSettingsPage {
settingsNavigationPath = NavigationPath()
settingsNavigationPath.append(page)
pendingSettingsPage = nil
}
} }
.onReceive(environments.openSettings) { .onReceive(environments.openSettings) {
viewModel.openSettings() viewModel.openSettings()
} }
.onReceive(NotificationCenter.default.publisher(for: .navigateToSettingsPage)) { notification in
guard let page = notification.object as? SettingsPage else { return }
pendingSettingsPage = page
if viewModel.selection == .settings {
settingsNavigationPath = NavigationPath()
settingsNavigationPath.append(page)
pendingSettingsPage = nil
} else {
viewModel.selection = .settings
}
}
.environment(\.selection, $viewModel.selection)
.environment(\.importProfile, $viewModel.importProfile) .environment(\.importProfile, $viewModel.importProfile)
.environment(\.importRemoteProfile, $viewModel.importRemoteProfile) .environment(\.importRemoteProfile, $viewModel.importRemoteProfile)
.environment(\.profileEditor, profileEditor) .environment(\.profileEditor, profileEditor)
+2 -9
View File
@@ -39,11 +39,8 @@ public class MainViewModel: BaseViewModel {
if url.host == "import-remote-profile" { if url.host == "import-remote-profile" {
var error: NSError? var error: NSError?
importRemoteProfile = LibboxParseRemoteProfileImportLink(url.absoluteString, &error) importRemoteProfile = LibboxParseRemoteProfileImportLink(url.absoluteString, &error)
if error != nil { if let error {
return alert = AlertState(error: error)
}
if selection != .dashboard {
selection = .dashboard
} }
} else if url.pathExtension == "bpf" { } else if url.pathExtension == "bpf" {
Task { Task {
@@ -61,10 +58,6 @@ public class MainViewModel: BaseViewModel {
url.stopAccessingSecurityScopedResource() url.stopAccessingSecurityScopedResource()
} catch { } catch {
alert = AlertState(error: error) alert = AlertState(error: error)
return
}
if selection != .dashboard {
selection = .dashboard
} }
} }
+1 -5
View File
@@ -192,16 +192,12 @@ public struct MenuView: View {
environments.selectedProfileUpdate.send() environments.selectedProfileUpdate.send()
if profile.status.isConnected { if profile.status.isConnected {
do { do {
try await serviceReload() try await profile.reloadService()
} catch { } catch {
alert = AlertState(error: error) alert = AlertState(error: error)
} }
} }
reasserting = false reasserting = false
} }
private nonisolated func serviceReload() async throws {
try LibboxNewStandaloneCommandClient()!.serviceReload()
}
} }
} }
+1 -1
View File
@@ -23,7 +23,7 @@ archive_ios:
upload_ios: upload_ios:
xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
release_maocs: archive_macos upload_macos release_macos: archive_macos upload_macos
archive_macos: archive_macos:
xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e " ARCHIVE FAILED" -e "❌" xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e " ARCHIVE FAILED" -e "❌"
+1 -8
View File
@@ -123,6 +123,7 @@ struct MainView: View {
environments.postReload() environments.postReload()
} }
.alert($alert) .alert($alert)
.globalChecks()
.onChangeCompat(of: scenePhase) { newValue in .onChangeCompat(of: scenePhase) { newValue in
if newValue == .active { if newValue == .active {
environments.postReload() environments.postReload()
@@ -185,10 +186,6 @@ struct MainView: View {
importRemoteProfile = LibboxParseRemoteProfileImportLink(url.absoluteString, &error) importRemoteProfile = LibboxParseRemoteProfileImportLink(url.absoluteString, &error)
if let error { if let error {
alert = AlertState(error: error) alert = AlertState(error: error)
return
}
if selection != .dashboard {
selection = .dashboard
} }
} else if url.pathExtension == "bpf" { } else if url.pathExtension == "bpf" {
do { do {
@@ -197,10 +194,6 @@ struct MainView: View {
url.stopAccessingSecurityScopedResource() url.stopAccessingSecurityScopedResource()
} catch { } catch {
alert = AlertState(error: error) alert = AlertState(error: error)
return
}
if selection != .dashboard {
selection = .dashboard
} }
} else { } else {
alert = AlertState(errorMessage: String(localized: "Handled unknown URL \(url.absoluteString)")) alert = AlertState(errorMessage: String(localized: "Handled unknown URL \(url.absoluteString)"))
+8
View File
@@ -113,5 +113,13 @@
<string>As a universal proxy platform, sing-box configures routing according to your configuration.</string> <string>As a universal proxy platform, sing-box configures routing according to your configuration.</string>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>Camera access is required to scan QR codes for importing profiles.</string> <string>Camera access is required to scan QR codes for importing profiles.</string>
<key>NSLocationUsageDescription</key>
<string>sing-box uses the Location permission to provide users with routing based on WIFI SSID and BSSID rules, without reading your location.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>sing-box uses the Location permission to provide users with routing based on WIFI SSID and BSSID rules, without reading your location.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>sing-box uses the Location permission to provide users with routing based on WIFI SSID and BSSID rules, without reading your location.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>sing-box uses the Location permission to provide users with routing based on WIFI SSID and BSSID rules, without reading your location.</string>
</dict> </dict>
</plist> </plist>
+2 -6
View File
@@ -20,17 +20,13 @@
<array> <array>
<string>iCloud.io.nekohasekai.sfavt</string> <string>iCloud.io.nekohasekai.sfavt</string>
</array> </array>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>$(TeamIdentifierPrefix)$(BASE_PACKAGE_IDENTIFIER)</string> <string>$(TeamIdentifierPrefix)$(BASE_PACKAGE_IDENTIFIER)</string>
</array> </array>
<key>com.apple.security.files.user-selected.read-write</key> <key>com.apple.developer.networking.wifi-info</key>
<true/> <true/>
<key>com.apple.security.network.client</key> <key>com.apple.security.personal-information.location</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/> <true/>
</dict> </dict>
</plist> </plist>
@@ -1,5 +1,6 @@
import AppKit import AppKit
import Foundation import Foundation
import Libbox
import Library import Library
import MacLibrary import MacLibrary
@@ -7,6 +8,8 @@ class StandaloneApplicationDelegate: ApplicationDelegate {
func applicationWillFinishLaunching(_: Notification) { func applicationWillFinishLaunching(_: Notification) {
Variant.useSystemExtension = true Variant.useSystemExtension = true
Variant.isBeta = false Variant.isBeta = false
LibboxSetXPCDialer(CommandXPCDialer.shared)
UserServiceEndpointPublisher.shared.start()
Task { Task {
await setupSystemExtension() await setupSystemExtension()
} }
+5
View File
@@ -7,6 +7,8 @@ struct MainView: View {
@Environment(\.scenePhase) private var scenePhase @Environment(\.scenePhase) private var scenePhase
@EnvironmentObject private var environments: ExtensionEnvironments @EnvironmentObject private var environments: ExtensionEnvironments
@State private var selection = NavigationPage.dashboard @State private var selection = NavigationPage.dashboard
@State private var importProfile: LibboxProfileContent?
@State private var importRemoteProfile: LibboxImportRemoteProfile?
var body: some View { var body: some View {
TabView(selection: $selection) { TabView(selection: $selection) {
@@ -32,6 +34,9 @@ struct MainView: View {
environments.connect() environments.connect()
} }
} }
.globalChecks()
.environment(\.selection, $selection) .environment(\.selection, $selection)
.environment(\.importProfile, $importProfile)
.environment(\.importRemoteProfile, $importRemoteProfile)
} }
} }
+1 -27
View File
@@ -1,30 +1,4 @@
import Libbox
import Library import Library
import NetworkExtension import NetworkExtension
import System
class PacketTunnelProvider: ExtensionProvider { class PacketTunnelProvider: ExtensionProvider {}
override func startTunnel(options: [String: NSObject]?) async throws {
guard let usernameObject = options?["username"] else {
throw ExtensionStartupError("missing start options")
}
let username = usernameObject as! NSString
FilePath.sharedDirectory = URL(filePath: "/Users/\(username)/Library/Group Containers/\(FilePath.groupName)")
FilePath.iCloudDirectory = URL(filePath: "/Users/\(username)/Library/Mobile Documents/iCloud~\(FilePath.packageName.replacingOccurrences(of: ".", with: "~"))").appendingPathComponent("Documents", isDirectory: true)
let databasePath = FilePath.sharedDirectory.appendingPathComponent("settings.db").relativePath
if !FileManager.default.isReadableFile(atPath: databasePath) {
do {
let fd = try FileDescriptor.open(databasePath, .readOnly)
try! fd.close()
NSLog("Can access \(databasePath)")
} catch {
NSLog("Can't access \(databasePath): \(error.localizedDescription)")
}
try await Task.sleep(nanoseconds: NSEC_PER_MSEC * 100)
throw FullDiskAccessPermissionRequired.error
}
self.username = String(username)
try await super.startTunnel(options: options)
}
}
+2 -2
View File
@@ -8,11 +8,11 @@
<array> <array>
<string>packet-tunnel-provider-systemextension</string> <string>packet-tunnel-provider-systemextension</string>
</array> </array>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>$(TeamIdentifierPrefix)$(BASE_PACKAGE_IDENTIFIER)</string> <string>$(TeamIdentifierPrefix)$(BASE_PACKAGE_IDENTIFIER)</string>
</array> </array>
<key>com.apple.security.app-sandbox</key>
<false/>
</dict> </dict>
</plist> </plist>
+234
View File
@@ -42,6 +42,9 @@
3AE396072C21A60C00647718 /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; }; 3AE396072C21A60C00647718 /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; };
3AE4D0BD2A6E2DDC009FEA9E /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; }; 3AE4D0BD2A6E2DDC009FEA9E /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; };
3AE4D0BE2A6E2DDC009FEA9E /* Library.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 3AE4D0BE2A6E2DDC009FEA9E /* Library.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
3AE595822F08C3D400C13426 /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; };
3AE595AA2F08C61600C13426 /* RootHelper in Copy Helpers */ = {isa = PBXBuildFile; fileRef = 3AE595712F08C34000C13426 /* RootHelper */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
3AE595B02F08D00000C13426 /* HelperService/LaunchDaemons/io.nekohasekai.sfavt.helper.plist in Copy LaunchDaemon */ = {isa = PBXBuildFile; fileRef = 3AE595AE2F08D00000C13426 /* HelperService/LaunchDaemons/io.nekohasekai.sfavt.helper.plist */; };
3AEAEE992A4F16430059612D /* Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 3A096F862A4ED3DE00D4A2ED /* Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 3AEAEE992A4F16430059612D /* Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 3A096F862A4ED3DE00D4A2ED /* Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
3AEECBF22A6DF40A006A0E0C /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AF342B12A4AA520002B34AC /* NetworkExtension.framework */; }; 3AEECBF22A6DF40A006A0E0C /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AF342B12A4AA520002B34AC /* NetworkExtension.framework */; };
3AEECC1A2A6DFA79006A0E0C /* io.nekohasekai.sfavt.system.systemextension in Embed System Extensions */ = {isa = PBXBuildFile; fileRef = 3AEECBF12A6DF40A006A0E0C /* io.nekohasekai.sfavt.system.systemextension */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 3AEECC1A2A6DFA79006A0E0C /* io.nekohasekai.sfavt.system.systemextension in Embed System Extensions */ = {isa = PBXBuildFile; fileRef = 3AEECBF12A6DF40A006A0E0C /* io.nekohasekai.sfavt.system.systemextension */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@@ -183,6 +186,20 @@
remoteGlobalIDString = 3AEC211C2A459B4700A63465; remoteGlobalIDString = 3AEC211C2A459B4700A63465;
remoteInfo = Library; remoteInfo = Library;
}; };
3AE595842F08C3D400C13426 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 3AEC20BD2A45991900A63465 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 3AEC211C2A459B4700A63465;
remoteInfo = Library;
};
3AE595A52F08C5C700C13426 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 3AEC20BD2A45991900A63465 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 3AE595702F08C34000C13426;
remoteInfo = RootHelper;
};
3AEAEE9A2A4F16430059612D /* PBXContainerItemProxy */ = { 3AEAEE9A2A4F16430059612D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy; isa = PBXContainerItemProxy;
containerPortal = 3AEC20BD2A45991900A63465 /* Project object */; containerPortal = 3AEC20BD2A45991900A63465 /* Project object */;
@@ -343,6 +360,37 @@
name = "Embed Frameworks"; name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
3AE5956F2F08C34000C13426 /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = /usr/share/man/man1/;
dstSubfolderSpec = 0;
files = (
);
runOnlyForDeploymentPostprocessing = 1;
};
3AE595A92F08C5E200C13426 /* Copy Helpers */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = ../Helpers;
dstSubfolderSpec = 6;
files = (
3AE595AA2F08C61600C13426 /* RootHelper in Copy Helpers */,
);
name = "Copy Helpers";
runOnlyForDeploymentPostprocessing = 0;
};
3AE595AC2F08C63000C13426 /* Copy LaunchDaemon */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = ../Library/LaunchDaemons;
dstSubfolderSpec = 7;
files = (
3AE595B02F08D00000C13426 /* HelperService/LaunchDaemons/io.nekohasekai.sfavt.helper.plist in Copy LaunchDaemon */,
);
name = "Copy LaunchDaemon";
runOnlyForDeploymentPostprocessing = 0;
};
3AEECC212A6DFA79006A0E0C /* Embed Frameworks */ = { 3AEECC212A6DFA79006A0E0C /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase; isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -384,6 +432,11 @@
3AE395F22C21A5CA00647718 /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 3AE395F22C21A5CA00647718 /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
3AE395F32C21A5CA00647718 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 3AE395F32C21A5CA00647718 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
3AE395F52C21A5CA00647718 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 3AE395F52C21A5CA00647718 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
3AE595712F08C34000C13426 /* RootHelper */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = RootHelper; sourceTree = BUILT_PRODUCTS_DIR; };
3AE595A12F08C4E900C13426 /* CoreWLAN.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreWLAN.framework; path = System/Library/Frameworks/CoreWLAN.framework; sourceTree = SDKROOT; };
3AE595A32F08C4ED00C13426 /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; };
3AE595AE2F08D00000C13426 /* HelperService/LaunchDaemons/io.nekohasekai.sfavt.helper.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = HelperService/LaunchDaemons/io.nekohasekai.sfavt.helper.plist; sourceTree = "<group>"; };
3AE595AF2F08D00000C13426 /* UserHelper/LaunchAgents/io.nekohasekai.sfavt.userhelper.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = UserHelper/LaunchAgents/io.nekohasekai.sfavt.userhelper.plist; sourceTree = "<group>"; };
3AEC20DB2A4599D000A63465 /* Libbox.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Libbox.xcframework; sourceTree = "<group>"; }; 3AEC20DB2A4599D000A63465 /* Libbox.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Libbox.xcframework; sourceTree = "<group>"; };
3AEC20F32A459AB400A63465 /* sing-box.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "sing-box.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 3AEC20F32A459AB400A63465 /* sing-box.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "sing-box.app"; sourceTree = BUILT_PRODUCTS_DIR; };
3AEC21092A459B1900A63465 /* sing-box.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "sing-box.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 3AEC21092A459B1900A63465 /* sing-box.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "sing-box.app"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -545,6 +598,7 @@
3ADDCFC62E8B72B4009ACE1D /* IntentsExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (3ADDCFC92E8B72B4009ACE1D /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = IntentsExtension; sourceTree = "<group>"; }; 3ADDCFC62E8B72B4009ACE1D /* IntentsExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (3ADDCFC92E8B72B4009ACE1D /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = IntentsExtension; sourceTree = "<group>"; };
3ADDCFCD2E8B72B8009ACE1D /* TVExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (3ADDCFCF2E8B72B8009ACE1D /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = TVExtension; sourceTree = "<group>"; }; 3ADDCFCD2E8B72B8009ACE1D /* TVExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (3ADDCFCF2E8B72B8009ACE1D /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = TVExtension; sourceTree = "<group>"; };
3ADDCFD52E8B72BC009ACE1D /* WidgetExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (3ADDCFDA2E8B72BC009ACE1D /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = WidgetExtension; sourceTree = "<group>"; }; 3ADDCFD52E8B72BC009ACE1D /* WidgetExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (3ADDCFDA2E8B72BC009ACE1D /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = WidgetExtension; sourceTree = "<group>"; };
3AE595722F08C34000C13426 /* HelperService */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = HelperService; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */ /* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -611,6 +665,14 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
3AE5956E2F08C34000C13426 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
3AE595822F08C3D400C13426 /* Library.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
3AEC20F02A459AB400A63465 /* Frameworks */ = { 3AEC20F02A459AB400A63465 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -676,6 +738,15 @@
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
3AE5C9042F08E53400C13426 /* Recovered References */ = {
isa = PBXGroup;
children = (
3AE595AE2F08D00000C13426 /* HelperService/LaunchDaemons/io.nekohasekai.sfavt.helper.plist */,
3AE595AF2F08D00000C13426 /* UserHelper/LaunchAgents/io.nekohasekai.sfavt.userhelper.plist */,
);
name = "Recovered References";
sourceTree = "<group>";
};
3AEC20BC2A45991900A63465 = { 3AEC20BC2A45991900A63465 = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -694,8 +765,10 @@
3ADDCFCD2E8B72B8009ACE1D /* TVExtension */, 3ADDCFCD2E8B72B8009ACE1D /* TVExtension */,
3ADDCFD52E8B72BC009ACE1D /* WidgetExtension */, 3ADDCFD52E8B72BC009ACE1D /* WidgetExtension */,
3AAAFB232EF5218F004C69AD /* FileProviderExtension */, 3AAAFB232EF5218F004C69AD /* FileProviderExtension */,
3AE595722F08C34000C13426 /* HelperService */,
3AEC20C72A45991900A63465 /* Products */, 3AEC20C72A45991900A63465 /* Products */,
3AEC21012A459AE300A63465 /* Frameworks */, 3AEC21012A459AE300A63465 /* Frameworks */,
3AE5C9042F08E53400C13426 /* Recovered References */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@@ -715,6 +788,7 @@
3AE171992A8128DD00393060 /* TVExtension.appex */, 3AE171992A8128DD00393060 /* TVExtension.appex */,
3AE395F22C21A5CA00647718 /* WidgetExtension.appex */, 3AE395F22C21A5CA00647718 /* WidgetExtension.appex */,
3AAAFB202EF5218F004C69AD /* FileProviderExtension.appex */, 3AAAFB202EF5218F004C69AD /* FileProviderExtension.appex */,
3AE595712F08C34000C13426 /* RootHelper */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -722,6 +796,8 @@
3AEC21012A459AE300A63465 /* Frameworks */ = { 3AEC21012A459AE300A63465 /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3AE595A32F08C4ED00C13426 /* UserNotifications.framework */,
3AE595A12F08C4E900C13426 /* CoreWLAN.framework */,
3AAAFE162EF52772004C69AD /* FileProvider.framework */, 3AAAFE162EF52772004C69AD /* FileProvider.framework */,
3AC72A2E2EED94DD0039DEA4 /* SystemConfiguration.framework */, 3AC72A2E2EED94DD0039DEA4 /* SystemConfiguration.framework */,
3AC72A302EED94F60039DEA4 /* SystemConfiguration.framework */, 3AC72A302EED94F60039DEA4 /* SystemConfiguration.framework */,
@@ -909,6 +985,29 @@
productReference = 3AE395F22C21A5CA00647718 /* WidgetExtension.appex */; productReference = 3AE395F22C21A5CA00647718 /* WidgetExtension.appex */;
productType = "com.apple.product-type.app-extension"; productType = "com.apple.product-type.app-extension";
}; };
3AE595702F08C34000C13426 /* RootHelper */ = {
isa = PBXNativeTarget;
buildConfigurationList = 3AE595752F08C34000C13426 /* Build configuration list for PBXNativeTarget "RootHelper" */;
buildPhases = (
3AE5956D2F08C34000C13426 /* Sources */,
3AE5956E2F08C34000C13426 /* Frameworks */,
3AE5956F2F08C34000C13426 /* CopyFiles */,
);
buildRules = (
);
dependencies = (
3AE595852F08C3D400C13426 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
3AE595722F08C34000C13426 /* HelperService */,
);
name = RootHelper;
packageProductDependencies = (
);
productName = RootHelper;
productReference = 3AE595712F08C34000C13426 /* RootHelper */;
productType = "com.apple.product-type.tool";
};
3AEC20F22A459AB400A63465 /* SFI */ = { 3AEC20F22A459AB400A63465 /* SFI */ = {
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 3AEC20FE2A459AB500A63465 /* Build configuration list for PBXNativeTarget "SFI" */; buildConfigurationList = 3AEC20FE2A459AB500A63465 /* Build configuration list for PBXNativeTarget "SFI" */;
@@ -1023,10 +1122,13 @@
3AEECC022A6DF9CA006A0E0C /* Resources */, 3AEECC022A6DF9CA006A0E0C /* Resources */,
3AEECC212A6DFA79006A0E0C /* Embed Frameworks */, 3AEECC212A6DFA79006A0E0C /* Embed Frameworks */,
3AEECC232A6DFA79006A0E0C /* Embed System Extensions */, 3AEECC232A6DFA79006A0E0C /* Embed System Extensions */,
3AE595A92F08C5E200C13426 /* Copy Helpers */,
3AE595AC2F08C63000C13426 /* Copy LaunchDaemon */,
); );
buildRules = ( buildRules = (
); );
dependencies = ( dependencies = (
3AE595A62F08C5C700C13426 /* PBXTargetDependency */,
3AEECC1C2A6DFA79006A0E0C /* PBXTargetDependency */, 3AEECC1C2A6DFA79006A0E0C /* PBXTargetDependency */,
3AEECC202A6DFA79006A0E0C /* PBXTargetDependency */, 3AEECC202A6DFA79006A0E0C /* PBXTargetDependency */,
3AEECC3D2A6DFDC6006A0E0C /* PBXTargetDependency */, 3AEECC3D2A6DFDC6006A0E0C /* PBXTargetDependency */,
@@ -1097,6 +1199,9 @@
3AE395F12C21A5CA00647718 = { 3AE395F12C21A5CA00647718 = {
CreatedOnToolsVersion = 16.0; CreatedOnToolsVersion = 16.0;
}; };
3AE595702F08C34000C13426 = {
CreatedOnToolsVersion = 26.2;
};
3AEC20F22A459AB400A63465 = { 3AEC20F22A459AB400A63465 = {
CreatedOnToolsVersion = 15.0; CreatedOnToolsVersion = 15.0;
}; };
@@ -1155,6 +1260,7 @@
3A77016C2A4E6B34008F031F /* IntentsExtension */, 3A77016C2A4E6B34008F031F /* IntentsExtension */,
3AE395F12C21A5CA00647718 /* WidgetExtension */, 3AE395F12C21A5CA00647718 /* WidgetExtension */,
3AAAFB1F2EF5218F004C69AD /* FileProviderExtension */, 3AAAFB1F2EF5218F004C69AD /* FileProviderExtension */,
3AE595702F08C34000C13426 /* RootHelper */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
@@ -1296,6 +1402,13 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
3AE5956D2F08C34000C13426 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
3AEC20EF2A459AB400A63465 /* Sources */ = { 3AEC20EF2A459AB400A63465 /* Sources */ = {
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -1426,6 +1539,16 @@
target = 3AEC211C2A459B4700A63465 /* Library */; target = 3AEC211C2A459B4700A63465 /* Library */;
targetProxy = 3AE4D0BA2A6E2C55009FEA9E /* PBXContainerItemProxy */; targetProxy = 3AE4D0BA2A6E2C55009FEA9E /* PBXContainerItemProxy */;
}; };
3AE595852F08C3D400C13426 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 3AEC211C2A459B4700A63465 /* Library */;
targetProxy = 3AE595842F08C3D400C13426 /* PBXContainerItemProxy */;
};
3AE595A62F08C5C700C13426 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 3AE595702F08C34000C13426 /* RootHelper */;
targetProxy = 3AE595A52F08C5C700C13426 /* PBXContainerItemProxy */;
};
3AEAEE9B2A4F16430059612D /* PBXTargetDependency */ = { 3AEAEE9B2A4F16430059612D /* PBXTargetDependency */ = {
isa = PBXTargetDependency; isa = PBXTargetDependency;
target = 3A096F852A4ED3DE00D4A2ED /* Extension */; target = 3A096F852A4ED3DE00D4A2ED /* Extension */;
@@ -1980,6 +2103,86 @@
}; };
name = Release; name = Release;
}; };
3AE595762F08C34000C13426 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
APP_GROUP_IDENTIFIER = "$(TeamIdentifierPrefix)$(BASE_PACKAGE_IDENTIFIER)";
AUTOMATION_APPLE_EVENTS = NO;
CODE_SIGN_ENTITLEMENTS = HelperService/RootHelper.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CREATE_INFOPLIST_SECTION_IN_BINARY = YES;
DEVELOPMENT_TEAM = 287TTNZF8L;
ENABLE_HARDENED_RUNTIME = NO;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO;
INFOPLIST_FILE = HelperService/Info.plist;
INFOPLIST_KEY_WKSupportsLiveActivityLaunchAttributeTypes = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_PACKAGE_IDENTIFIER).helper";
PRODUCT_NAME = "$(TARGET_NAME)";
RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO;
RUNTIME_EXCEPTION_ALLOW_JIT = NO;
RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO;
RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO;
RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO;
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
SDKROOT = macosx;
SKIP_INSTALL = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
3AE595772F08C34000C13426 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
APP_GROUP_IDENTIFIER = "$(TeamIdentifierPrefix)$(BASE_PACKAGE_IDENTIFIER)";
AUTOMATION_APPLE_EVENTS = NO;
CODE_SIGN_ENTITLEMENTS = HelperService/RootHelper.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CREATE_INFOPLIST_SECTION_IN_BINARY = YES;
DEVELOPMENT_TEAM = 287TTNZF8L;
ENABLE_HARDENED_RUNTIME = NO;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO;
INFOPLIST_FILE = HelperService/Info.plist;
INFOPLIST_KEY_WKSupportsLiveActivityLaunchAttributeTypes = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_PACKAGE_IDENTIFIER).helper";
PRODUCT_NAME = "$(TARGET_NAME)";
RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO;
RUNTIME_EXCEPTION_ALLOW_JIT = NO;
RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO;
RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO;
RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO;
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
SDKROOT = macosx;
SKIP_INSTALL = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
};
name = Release;
};
3AEC20CB2A45991900A63465 /* Debug */ = { 3AEC20CB2A45991900A63465 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
@@ -2379,7 +2582,18 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=macosx*]" = 287TTNZF8L; "DEVELOPMENT_TEAM[sdk=macosx*]" = 287TTNZF8L;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SystemExtension/Info.plist; INFOPLIST_FILE = SystemExtension/Info.plist;
@@ -2416,7 +2630,18 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=macosx*]" = 287TTNZF8L; "DEVELOPMENT_TEAM[sdk=macosx*]" = 287TTNZF8L;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PRINTING = NO;
ENABLE_RESOURCE_ACCESS_USB = NO;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SystemExtension/Info.plist; INFOPLIST_FILE = SystemExtension/Info.plist;
@@ -2669,6 +2894,15 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
3AE595752F08C34000C13426 /* Build configuration list for PBXNativeTarget "RootHelper" */ = {
isa = XCConfigurationList;
buildConfigurations = (
3AE595762F08C34000C13426 /* Debug */,
3AE595772F08C34000C13426 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
3AEC20C02A45991900A63465 /* Build configuration list for PBXProject "sing-box" */ = { 3AEC20C02A45991900A63465 /* Build configuration list for PBXProject "sing-box" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (