refactor: Fix macOS standalone application
This commit is contained in:
@@ -100,7 +100,7 @@ public class ProfileServer {
|
||||
try await processProfileContentRequest(data)
|
||||
}
|
||||
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)
|
||||
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()
|
||||
content.name = profile.name
|
||||
|
||||
@@ -19,7 +19,7 @@ import Library
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,121 +1,8 @@
|
||||
import Library
|
||||
@_exported import struct Library.AlertState
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func alert(_ binding: Binding<AlertState?>, isLoading: Binding<Bool>) -> some View {
|
||||
alert(
|
||||
|
||||
@@ -153,3 +153,15 @@ public func FormNavigationLink(@ViewBuilder destination: () -> some View, @ViewB
|
||||
}, label: label)
|
||||
#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())
|
||||
}
|
||||
}
|
||||
@@ -511,10 +511,7 @@ extension ProfileCard {
|
||||
try await profile.updateRemoteProfile()
|
||||
environments.profileUpdate.send()
|
||||
} catch {
|
||||
alert = AlertState(
|
||||
title: String(localized: "Update Failed"),
|
||||
message: error.localizedDescription
|
||||
)
|
||||
alert = AlertState(error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,10 +402,7 @@ struct ProfilePickerSheet: View {
|
||||
try await profile.origin.updateRemoteProfile()
|
||||
environments.profileUpdate.send()
|
||||
} catch {
|
||||
alert = AlertState(
|
||||
title: String(localized: "Update Failed"),
|
||||
message: error.localizedDescription
|
||||
)
|
||||
alert = AlertState(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,8 @@ import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public struct DashboardView: View {
|
||||
@Environment(\.openURL) private var openURL
|
||||
@Environment(\.importProfile) private var importProfile
|
||||
@Environment(\.importRemoteProfile) private var importRemoteProfile
|
||||
@EnvironmentObject private var environments: ExtensionEnvironments
|
||||
@StateObject private var coordinator = DashboardViewModel()
|
||||
@State private var importRemoteProfileRequest: NewProfileView.ImportRequest?
|
||||
|
||||
#if os(macOS)
|
||||
@Environment(\.controlActiveState) private var controlActiveState
|
||||
@@ -19,24 +15,16 @@ public struct DashboardView: View {
|
||||
|
||||
public var body: some View {
|
||||
content
|
||||
.alert($coordinator.alert)
|
||||
.onAppear {
|
||||
coordinator.setOpenURL { openURL($0) }
|
||||
coordinator.setEnvironments(environments)
|
||||
#if os(macOS)
|
||||
Task { await coordinator.reload() }
|
||||
#endif
|
||||
handleImportProfile()
|
||||
handleImportRemoteProfile()
|
||||
}
|
||||
.onChangeCompat(of: importProfile.wrappedValue) { _ in
|
||||
handleImportProfile()
|
||||
}
|
||||
.onChangeCompat(of: importRemoteProfile.wrappedValue) { _ in
|
||||
handleImportRemoteProfile()
|
||||
}
|
||||
#if os(tvOS)
|
||||
.navigationDestination(item: $importRemoteProfileRequest) { request in
|
||||
NewProfileView(request)
|
||||
.navigationDestination(item: $environments.pendingImportRemoteProfile) { request in
|
||||
NewProfileView(.init(name: request.name, url: request.url))
|
||||
.environmentObject(environments)
|
||||
.onDisappear {
|
||||
environments.profileUpdate.send()
|
||||
@@ -48,7 +36,7 @@ public struct DashboardView: View {
|
||||
}
|
||||
}
|
||||
#else
|
||||
.sheet(item: $importRemoteProfileRequest) { request in
|
||||
.sheet(item: $environments.pendingImportRemoteProfile) { request in
|
||||
importRemoteProfileSheet(for: request)
|
||||
}
|
||||
#endif
|
||||
@@ -60,48 +48,12 @@ public struct DashboardView: View {
|
||||
#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
|
||||
private func importRemoteProfileSheet(for request: NewProfileView.ImportRequest) -> some View {
|
||||
private func importRemoteProfileSheet(for request: ImportRemoteProfileRequest) -> some View {
|
||||
NavigationSheet(title: "Import Profile", onDismiss: {
|
||||
environments.profileUpdate.send()
|
||||
}, content: {
|
||||
NewProfileView(request)
|
||||
NewProfileView(.init(name: request.name, url: request.url))
|
||||
.environmentObject(environments)
|
||||
})
|
||||
}
|
||||
@@ -132,9 +84,13 @@ public struct DashboardView: View {
|
||||
} else if let profile = environments.extensionProfile {
|
||||
activeDashboardView
|
||||
.environmentObject(profile)
|
||||
.alert($coordinator.alert)
|
||||
.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 {
|
||||
FormView {
|
||||
|
||||
@@ -2,12 +2,15 @@ import Foundation
|
||||
import Libbox
|
||||
import Library
|
||||
import NetworkExtension
|
||||
import os
|
||||
import SwiftUI
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
private let logger = Logger(category: "DashboardViewModel")
|
||||
|
||||
@MainActor
|
||||
public final class DashboardViewModel: BaseViewModel {
|
||||
@Published public var profileList: [ProfilePreview] = []
|
||||
@@ -15,14 +18,12 @@ public final class DashboardViewModel: BaseViewModel {
|
||||
@Published public var selection = DashboardPage.overview
|
||||
@Published public var systemProxyAvailable = false
|
||||
@Published public var systemProxyEnabled = false
|
||||
@Published public var notStarted = false
|
||||
|
||||
#if os(macOS)
|
||||
@Published public var systemExtensionInstalled = true
|
||||
#endif
|
||||
|
||||
private weak var environments: ExtensionEnvironments?
|
||||
private var openURL: ((URL) -> Void)?
|
||||
|
||||
public func setEnvironments(_ environments: ExtensionEnvironments) {
|
||||
self.environments = environments
|
||||
@@ -33,10 +34,6 @@ public final class DashboardViewModel: BaseViewModel {
|
||||
isLoading = true
|
||||
}
|
||||
|
||||
public func setOpenURL(_ openURL: @escaping (URL) -> Void) {
|
||||
self.openURL = openURL
|
||||
}
|
||||
|
||||
public func reload() async {
|
||||
#if os(macOS)
|
||||
if Variant.useSystemExtension {
|
||||
@@ -86,84 +83,13 @@ public final class DashboardViewModel: BaseViewModel {
|
||||
systemProxyAvailable = status.available
|
||||
systemProxyEnabled = status.enabled
|
||||
} catch {
|
||||
NSLog("reloadSystemProxy: \(error)")
|
||||
logger.debug("reloadSystemProxy: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
public func updateSelectedProfile() async {
|
||||
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, *)
|
||||
@@ -184,7 +110,7 @@ extension ExtensionProfile {
|
||||
)
|
||||
}
|
||||
#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 {
|
||||
do {
|
||||
try await serviceReload()
|
||||
try await profile.reloadService()
|
||||
} catch {
|
||||
alert = AlertState(error: error)
|
||||
}
|
||||
@@ -21,10 +21,6 @@ public final class OverviewViewModel: BaseViewModel {
|
||||
reasserting = false
|
||||
}
|
||||
|
||||
public nonisolated func serviceReload() async throws {
|
||||
try LibboxNewStandaloneCommandClient()!.serviceReload()
|
||||
}
|
||||
|
||||
public nonisolated func setSystemProxyEnabled(_ enabled: Bool, profile: ExtensionProfile) async {
|
||||
do {
|
||||
await SharedPreferences.systemProxyEnabled.set(enabled)
|
||||
|
||||
@@ -86,7 +86,9 @@ private struct LogViewContent: View {
|
||||
let button = UIButton(type: .system)
|
||||
let config = UIImage.SymbolConfiguration(scale: .large)
|
||||
button.setImage(UIImage(systemName: "line.3.horizontal.circle", withConfiguration: config), for: .normal)
|
||||
if #available(iOS 17.0, *) {
|
||||
button.tintColor = colorScheme == .dark ? .white : .black
|
||||
}
|
||||
button.showsMenuAsPrimaryAction = true
|
||||
button.menu = createMenu()
|
||||
button.setContentHuggingPriority(.required, for: .horizontal)
|
||||
@@ -96,8 +98,10 @@ private struct LogViewContent: View {
|
||||
|
||||
func updateUIView(_ uiView: UIButton, context _: Context) {
|
||||
uiView.menu = createMenu()
|
||||
if #available(iOS 17.0, *) {
|
||||
uiView.tintColor = colorScheme == .dark ? .white : .black
|
||||
}
|
||||
}
|
||||
|
||||
private func createMenu() -> UIMenu {
|
||||
let logLevelActions = [
|
||||
|
||||
@@ -86,9 +86,11 @@ public class LogDataModel: ObservableObject {
|
||||
lastProcessedLogCount = 0
|
||||
lastEffectiveLevel = nil
|
||||
lastSearchText = ""
|
||||
filteredLogs = []
|
||||
visibleLogs = []
|
||||
commandClient.clearLogs()
|
||||
Task.detached {
|
||||
let client = LibboxNewStandaloneCommandClient()
|
||||
try? client?.clearLogs()
|
||||
try? LibboxNewStandaloneCommandClient()!.clearLogs()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,10 +39,10 @@ public final class EditProfileContentViewModel: BaseViewModel {
|
||||
|
||||
private nonisolated func loadContentBackground() async throws {
|
||||
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 {
|
||||
throw NSError(domain: "Profile missing", code: 0)
|
||||
throw NSError(domain: "EditProfileContentViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Profile missing")])
|
||||
}
|
||||
let profileContent = try profile.read()
|
||||
await MainActor.run {
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
do {
|
||||
message = try socket.read()
|
||||
} 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?
|
||||
switch Int64(message[0]) {
|
||||
@@ -71,7 +71,7 @@
|
||||
throw error
|
||||
}
|
||||
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:
|
||||
let decoder = LibboxProfileDecoder()
|
||||
@@ -97,7 +97,7 @@
|
||||
try await importProfile(content!, environments: environments)
|
||||
return
|
||||
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?
|
||||
let remoteProfile = LibboxParseRemoteProfileImportLink(string, &error)
|
||||
if let error {
|
||||
alert = AlertState(
|
||||
title: String(localized: "Invalid QR Code"),
|
||||
message: error.localizedDescription
|
||||
)
|
||||
alert = AlertState(error: error)
|
||||
return
|
||||
}
|
||||
guard let remoteProfile else {
|
||||
alert = AlertState(
|
||||
title: String(localized: "Invalid QR Code"),
|
||||
message: String(localized: "The QR code does not contain a valid profile import link.")
|
||||
)
|
||||
alert = AlertState(errorMessage: String(localized: "The QR code does not contain a valid profile import link."))
|
||||
return
|
||||
}
|
||||
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")
|
||||
if fileImport {
|
||||
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() {
|
||||
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 {
|
||||
fileURL.stopAccessingSecurityScopedResource()
|
||||
|
||||
@@ -145,10 +145,7 @@
|
||||
message: String(localized: "Please enable camera access in Settings to scan QR codes.")
|
||||
)
|
||||
default:
|
||||
alert = AlertState(
|
||||
title: String(localized: "Scanner Error"),
|
||||
message: error.localizedDescription
|
||||
)
|
||||
alert = AlertState(error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import SwiftUI
|
||||
#if os(iOS)
|
||||
import FileProvider
|
||||
import UIKit
|
||||
#elseif os(macOS)
|
||||
import ServiceManagement
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
@@ -15,7 +17,11 @@ public struct CoreView: View {
|
||||
@State private var disableDeprecatedWarnings = false
|
||||
|
||||
@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 var body: some View {
|
||||
@@ -29,7 +35,21 @@ public struct CoreView: View {
|
||||
} else {
|
||||
FormView {
|
||||
FormTextItem("Version", version)
|
||||
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 {
|
||||
Section {}
|
||||
@@ -40,11 +60,13 @@ public struct CoreView: View {
|
||||
|
||||
Section("Working Directory") {
|
||||
#if os(macOS)
|
||||
if !Variant.useSystemExtension {
|
||||
FormButton {
|
||||
NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: FilePath.workingDirectory.relativePath)
|
||||
} label: {
|
||||
Label("Open", systemImage: "macwindow.and.cursorarrow")
|
||||
}
|
||||
}
|
||||
#elseif os(iOS)
|
||||
if #available(iOS 16.0, *) {
|
||||
FormButton {
|
||||
@@ -85,7 +107,11 @@ public struct CoreView: View {
|
||||
} else {
|
||||
await MainActor.run {
|
||||
version = LibboxVersion()
|
||||
dataSize = "Loading..."
|
||||
#if os(macOS)
|
||||
if Variant.useSystemExtension {
|
||||
helperUnavailable = HelperServiceManager.rootHelperStatus != .enabled
|
||||
}
|
||||
#endif
|
||||
isLoading = false
|
||||
}
|
||||
await loadSettingsBackground()
|
||||
@@ -94,7 +120,20 @@ public struct CoreView: View {
|
||||
|
||||
private nonisolated func loadSettingsBackground() async {
|
||||
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 {
|
||||
self.disableDeprecatedWarnings = disableDeprecatedWarnings
|
||||
self.dataSize = dataSize
|
||||
@@ -102,6 +141,12 @@ public struct CoreView: View {
|
||||
}
|
||||
|
||||
private func confirmDestroyWorkingDirectory() async {
|
||||
#if os(macOS)
|
||||
if helperUnavailable {
|
||||
alert = helperRequiredAlert()
|
||||
return
|
||||
}
|
||||
#endif
|
||||
if environments.extensionProfile?.status.isConnected == true {
|
||||
alert = AlertState(
|
||||
title: String(localized: "Service is Running"),
|
||||
@@ -119,17 +164,44 @@ public struct CoreView: View {
|
||||
}
|
||||
|
||||
private func stopServiceAndDestroy() async {
|
||||
try? await environments.extensionProfile?.stop()
|
||||
do {
|
||||
try await environments.extensionProfile!.stop()
|
||||
await destroyWorkingDirectory()
|
||||
} catch {
|
||||
alert = AlertState(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated func destroyWorkingDirectory() async {
|
||||
try? FileManager.default.removeItem(at: FilePath.workingDirectory)
|
||||
await MainActor.run {
|
||||
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)
|
||||
@available(iOS 16.0, *)
|
||||
private nonisolated func openInFilesApp() async {
|
||||
|
||||
@@ -12,6 +12,11 @@ public struct AppView: View {
|
||||
@State private var startAtLogin = false
|
||||
@Environment(\.showMenuBarExtra) private var showMenuBarExtra
|
||||
@State private var menuBarExtraInBackground = false
|
||||
#if os(macOS)
|
||||
|
||||
@State private var rootHelperRegistrationStatus: SMAppService.Status = .notRegistered
|
||||
|
||||
#endif
|
||||
|
||||
@State private var alert: AlertState?
|
||||
|
||||
@@ -67,6 +72,48 @@ public struct AppView: View {
|
||||
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
|
||||
}
|
||||
@@ -83,6 +130,9 @@ public struct AppView: View {
|
||||
#if os(macOS)
|
||||
startAtLogin = SMAppService.mainApp.status == .enabled
|
||||
menuBarExtraInBackground = await SharedPreferences.menuBarExtraInBackground.get()
|
||||
if Variant.useSystemExtension {
|
||||
refreshHelperStatus()
|
||||
}
|
||||
#endif
|
||||
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
|
||||
}
|
||||
|
||||
@@ -296,7 +296,7 @@ public struct OnDemandRulesView: View {
|
||||
await SharedPreferences.onDemandRules.set(rules)
|
||||
let savedRules = await SharedPreferences.onDemandRules.get()
|
||||
if savedRules != rules {
|
||||
alert = AlertState(errorMessage: "Failed to save rules")
|
||||
alert = AlertState(errorMessage: String(localized: "Failed to save rules"))
|
||||
return
|
||||
}
|
||||
await updateService()
|
||||
|
||||
@@ -19,7 +19,7 @@ struct PacketTunnelView: View {
|
||||
Group {
|
||||
if isLoading {
|
||||
ProgressView().onAppear {
|
||||
Task.detached {
|
||||
Task {
|
||||
await loadSettings()
|
||||
}
|
||||
}
|
||||
@@ -115,6 +115,7 @@ struct PacketTunnelView: View {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func loadSettings() async {
|
||||
ignoreMemoryLimit = await SharedPreferences.ignoreMemoryLimit.get()
|
||||
#if !os(tvOS)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
@@ -15,7 +14,7 @@ public struct ProfileOverrideView: View {
|
||||
Group {
|
||||
if isLoading {
|
||||
ProgressView().onAppear {
|
||||
Task.detached {
|
||||
Task {
|
||||
await loadSettings()
|
||||
}
|
||||
}
|
||||
@@ -64,12 +63,13 @@ public struct ProfileOverrideView: View {
|
||||
return
|
||||
}
|
||||
do {
|
||||
try LibboxNewStandaloneCommandClient()?.serviceReload()
|
||||
try await profile.reloadService()
|
||||
} catch {
|
||||
alert = AlertState(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func loadSettings() async {
|
||||
excludeDefaultRoute = await SharedPreferences.excludeDefaultRoute.get()
|
||||
autoRouteUseSubRangesByDefault = await SharedPreferences.autoRouteUseSubRangesByDefault.get()
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
import Library
|
||||
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 {
|
||||
private enum Tabs: Int, CaseIterable, Identifiable {
|
||||
var id: Self {
|
||||
@@ -13,6 +33,25 @@ public struct SettingView: View {
|
||||
|
||||
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 {
|
||||
Label(title, systemImage: iconImage)
|
||||
}
|
||||
@@ -83,16 +122,45 @@ public struct SettingView: View {
|
||||
|
||||
@MainActor
|
||||
var navigationLink: some View {
|
||||
#if os(macOS)
|
||||
FormNavigationLink(value: page) {
|
||||
label
|
||||
}
|
||||
#else
|
||||
FormNavigationLink {
|
||||
contentView
|
||||
} label: {
|
||||
label
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#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 var body: some View {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
var isDir: ObjCBool = false
|
||||
FileManager.default.fileExists(atPath: url.path, isDirectory: &isDir)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
let service = RootHelperService()
|
||||
service.start()
|
||||
dispatchMain()
|
||||
@@ -1,6 +1,5 @@
|
||||
import AppIntents
|
||||
import Foundation
|
||||
import Libbox
|
||||
import Library
|
||||
|
||||
struct StartServiceIntent: AppIntent {
|
||||
@@ -18,7 +17,7 @@ struct StartServiceIntent: AppIntent {
|
||||
|
||||
func perform() async throws -> some IntentResult & ProvidesDialog {
|
||||
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 specifiedProfile = profileList.first { $0.name == profile }
|
||||
@@ -30,13 +29,13 @@ struct StartServiceIntent: AppIntent {
|
||||
profileChanged = true
|
||||
}
|
||||
} 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 !profileChanged {
|
||||
return .result(dialog: "Service is already running")
|
||||
}
|
||||
try LibboxNewStandaloneCommandClient()!.serviceReload()
|
||||
try await extensionProfile.reloadService()
|
||||
} else if await extensionProfile.status.isConnected {
|
||||
try await extensionProfile.restart()
|
||||
} else {
|
||||
@@ -61,7 +60,7 @@ struct RestartServiceIntent: AppIntent {
|
||||
return .result(dialog: "Service is not installed")
|
||||
}
|
||||
if await extensionProfile.status == .connected {
|
||||
try LibboxNewStandaloneCommandClient()!.serviceReload()
|
||||
try await extensionProfile.reloadService()
|
||||
} else if await extensionProfile.status.isConnected {
|
||||
try await extensionProfile.restart()
|
||||
} else {
|
||||
@@ -145,7 +144,7 @@ struct GetCurrentProfile: AppIntent {
|
||||
|
||||
func perform() async throws -> some IntentResult & ReturnsValue<String> {
|
||||
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)
|
||||
}
|
||||
@@ -167,10 +166,10 @@ struct UpdateProfileIntent: AppIntent {
|
||||
init() {}
|
||||
func perform() async throws -> some IntentResult & ProvidesDialog {
|
||||
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 {
|
||||
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()
|
||||
return .result(dialog: "Profile updated")
|
||||
|
||||
@@ -10,6 +10,7 @@ public extension Date {
|
||||
var relativeFormat: String {
|
||||
let formatter = RelativeDateTimeFormatter()
|
||||
formatter.unitsStyle = .full
|
||||
formatter.dateTimeStyle = .named
|
||||
return formatter.localizedString(for: self, relativeTo: Date())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ public extension UTType {
|
||||
}
|
||||
|
||||
#if !os(tvOS)
|
||||
|
||||
// MARK: - FileDocument for Export
|
||||
|
||||
public struct ProfileExportDocument: FileDocument {
|
||||
|
||||
@@ -29,7 +29,7 @@ public extension Profile {
|
||||
if await SharedPreferences.selectedProfileID.get() == id {
|
||||
if let profile = try? await ExtensionProfile.load() {
|
||||
if await profile.status == .connected {
|
||||
try LibboxNewStandaloneCommandClient()!.serviceReload()
|
||||
try await profile.reloadService()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import BinaryCodable
|
||||
import Foundation
|
||||
import GRDB
|
||||
import os
|
||||
|
||||
private let logger = Logger(category: "SharedPreferences")
|
||||
|
||||
extension SharedPreferences {
|
||||
public class Preference<T: Codable> {
|
||||
@@ -16,7 +19,7 @@ extension SharedPreferences {
|
||||
do {
|
||||
return try await SharedPreferences.read(name) ?? defaultValue
|
||||
} catch {
|
||||
NSLog("read preferences error: \(error)")
|
||||
logger.error("read preferences error: \(error)")
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
@@ -31,7 +34,7 @@ extension SharedPreferences {
|
||||
do {
|
||||
try await SharedPreferences.write(name, newValue)
|
||||
} catch {
|
||||
NSLog("write preferences error: \(error)")
|
||||
logger.error("write preferences error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import Foundation
|
||||
import Libbox
|
||||
import os
|
||||
|
||||
private let logger = Logger(category: "CommandClient")
|
||||
|
||||
public struct LogEntry: Identifiable {
|
||||
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() {
|
||||
guard let message = rawConnections else {
|
||||
return
|
||||
@@ -222,7 +232,7 @@ public class CommandClient: ObservableObject {
|
||||
commandClient.isConnected = false
|
||||
}
|
||||
if let message {
|
||||
NSLog("client disconnected: \(message)")
|
||||
logger.debug("client disconnected: \(message)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +244,7 @@ public class CommandClient: ObservableObject {
|
||||
|
||||
func clearLogs() {
|
||||
DispatchQueue.main.async { [self] in
|
||||
commandClient.logList.removeAll()
|
||||
commandClient.clearLogs()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -1,12 +1,141 @@
|
||||
import Foundation
|
||||
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
|
||||
public class ExtensionEnvironments: ObservableObject {
|
||||
@Published public var commandClient = CommandClient([.log, .status, .groups, .clashMode, .connections])
|
||||
@Published public var extensionProfileLoading = true
|
||||
@Published public var extensionProfile: ExtensionProfile?
|
||||
@Published public var emptyProfiles = false
|
||||
@Published public var pendingImportRemoteProfile: ImportRemoteProfileRequest?
|
||||
|
||||
public let profileUpdate = ObjectWillChangePublisher()
|
||||
public let selectedProfileUpdate = ObjectWillChangePublisher()
|
||||
@@ -14,12 +143,6 @@ public class ExtensionEnvironments: ObservableObject {
|
||||
|
||||
public init() {}
|
||||
|
||||
nonisolated deinit {
|
||||
Task { @MainActor in
|
||||
commandClient.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
public func postReload() {
|
||||
Task {
|
||||
await reload()
|
||||
|
||||
@@ -22,14 +22,17 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
||||
|
||||
private func openTun0(_ options: LibboxTunOptionsProtocol?, _ ret0_: UnsafeMutablePointer<Int32>?) async throws {
|
||||
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 {
|
||||
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 excludeAPNs = await SharedPreferences.excludeAPNsRoute.get()
|
||||
let prefs = tunnel.overridePreferences ?? ExtensionProvider.OverridePreferences()
|
||||
let autoRouteUseSubRangesByDefault = prefs.autoRouteUseSubRangesByDefault
|
||||
let excludeAPNs = prefs.excludeAPNsRoute
|
||||
let excludeDefaultRoute = prefs.excludeDefaultRoute
|
||||
let systemProxyEnabled = prefs.systemProxyEnabled
|
||||
|
||||
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
|
||||
if options.getAutoRoute() {
|
||||
@@ -78,7 +81,7 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
||||
let ipv4RoutePrefix = inet4RouteExcludeAddressIterator.next()!
|
||||
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
|
||||
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())))
|
||||
}
|
||||
|
||||
if await SharedPreferences.excludeDefaultRoute.get(), !ipv6Routes.isEmpty {
|
||||
if excludeDefaultRoute, !ipv6Routes.isEmpty {
|
||||
if !ipv6ExcludeRoutes.contains(where: { it in
|
||||
it.destinationAddress == "::" && it.destinationNetworkPrefixLength == 127
|
||||
}) {
|
||||
@@ -152,7 +155,7 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
||||
let proxyServer = NEProxyServer(address: options.getHTTPProxyServer(), port: Int(options.getHTTPProxyServerPort()))
|
||||
proxySettings.httpServer = proxyServer
|
||||
proxySettings.httpsServer = proxyServer
|
||||
if await SharedPreferences.systemProxyEnabled.get() {
|
||||
if systemProxyEnabled {
|
||||
proxySettings.httpEnabled = true
|
||||
proxySettings.httpsEnabled = true
|
||||
}
|
||||
@@ -194,7 +197,7 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
||||
if tunFdFromLoop != -1 {
|
||||
ret0_.pointee = tunFdFromLoop
|
||||
} 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 findConnectionOwner(_: Int32, sourceAddress _: String?, sourcePort _: Int32, destinationAddress _: String?, destinationPort _: Int32, ret0_ _: UnsafeMutablePointer<Int32>?) throws {
|
||||
throw NSError(domain: "not implemented", code: 0)
|
||||
public func findConnectionOwner(_ ipProtocol: Int32, sourceAddress: String?, sourcePort: Int32, destinationAddress: String?, destinationPort: Int32) throws -> LibboxConnectionOwner {
|
||||
#if os(macOS)
|
||||
if Variant.useSystemExtension {
|
||||
guard let sourceAddress, let destinationAddress else {
|
||||
throw NSError(domain: "findConnectionOwner", code: 0, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Missing source or destination address",
|
||||
])
|
||||
}
|
||||
|
||||
public func packageName(byUid _: Int32, error _: NSErrorPointer) -> String {
|
||||
""
|
||||
let owner = try RootHelperClient.shared.findConnectionOwner(
|
||||
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
|
||||
}
|
||||
|
||||
public func uid(byPackageName _: String?, ret0_ _: UnsafeMutablePointer<Int32>?) throws {
|
||||
throw NSError(domain: "not implemented", code: 0)
|
||||
#endif
|
||||
throw NSError(domain: "ExtensionPlatformInterface", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Not implemented")])
|
||||
}
|
||||
|
||||
public func useProcFS() -> Bool {
|
||||
@@ -264,7 +280,7 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
||||
|
||||
public func getInterfaces() throws -> LibboxNetworkInterfaceIteratorProtocol {
|
||||
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
|
||||
if path.status == .unsatisfied {
|
||||
@@ -313,10 +329,10 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
||||
}
|
||||
|
||||
public func includeAllNetworks() -> Bool {
|
||||
#if !os(tvOS)
|
||||
return SharedPreferences.includeAllNetworks.getBlocking()
|
||||
#else
|
||||
#if os(tvOS)
|
||||
return false
|
||||
#else
|
||||
return tunnel.overridePreferences?.includeAllNetworks ?? false
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -324,12 +340,20 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
||||
guard let networkSettings else {
|
||||
return
|
||||
}
|
||||
tunnel.reasserting = true
|
||||
tunnel.setTunnelNetworkSettings(nil) { _ in
|
||||
runBlocking {
|
||||
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? {
|
||||
@@ -342,6 +366,9 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
||||
}
|
||||
return LibboxWIFIState(network.ssid, wifiBSSID: network.bssid)!
|
||||
#elseif os(macOS)
|
||||
if Variant.useSystemExtension {
|
||||
return UserServiceClient.shared.readWIFIState()
|
||||
}
|
||||
guard let interface = CWWiFiClient.shared().interface() else {
|
||||
return nil
|
||||
}
|
||||
@@ -425,6 +452,8 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
||||
|
||||
func reset() {
|
||||
networkSettings = nil
|
||||
nwMonitor?.cancel()
|
||||
nwMonitor = nil
|
||||
}
|
||||
|
||||
public func send(_ notification: LibboxNotification?) throws {
|
||||
@@ -432,6 +461,12 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
|
||||
guard let notification else {
|
||||
return
|
||||
}
|
||||
#if os(macOS)
|
||||
if Variant.useSystemExtension {
|
||||
try UserServiceClient.shared.sendNotification(notification)
|
||||
return
|
||||
}
|
||||
#endif
|
||||
let center = UNUserNotificationCenter.current()
|
||||
let content = UNMutableNotificationContent()
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import Foundation
|
||||
import Libbox
|
||||
import NetworkExtension
|
||||
import os
|
||||
|
||||
private let logger = Logger(category: "ExtensionProfile")
|
||||
|
||||
@MainActor
|
||||
public class ExtensionProfile: ObservableObject {
|
||||
@@ -24,12 +27,13 @@ public class ExtensionProfile: ObservableObject {
|
||||
observer = NotificationCenter.default.addObserver(
|
||||
forName: NSNotification.Name.NEVPNStatusDidChange,
|
||||
object: manager.connection,
|
||||
queue: .main
|
||||
queue: nil
|
||||
) { [weak self] notification in
|
||||
guard let self else {
|
||||
guard let connection = notification.object as? NEVPNConnection else {
|
||||
return
|
||||
}
|
||||
guard let connection = notification.object as? NEVPNConnection else {
|
||||
Task { @MainActor in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.connection = connection
|
||||
@@ -37,6 +41,7 @@ public class ExtensionProfile: ObservableObject {
|
||||
self.connectedDate = connection.connectedDate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func unregister() {
|
||||
if let observer {
|
||||
@@ -78,7 +83,7 @@ public class ExtensionProfile: ObservableObject {
|
||||
}
|
||||
|
||||
public func start() async throws {
|
||||
await fetchProfile()
|
||||
try await fetchProfile()
|
||||
manager.isEnabled = true
|
||||
let alwaysOn = await SharedPreferences.alwaysOn.get()
|
||||
let onDemandEnabled = await SharedPreferences.onDemandEnabled.get()
|
||||
@@ -96,30 +101,74 @@ public class ExtensionProfile: ObservableObject {
|
||||
}
|
||||
#endif
|
||||
try await manager.saveToPreferences()
|
||||
#if os(macOS)
|
||||
if Variant.useSystemExtension {
|
||||
try manager.connection.startVPNTunnel(options: [
|
||||
"username": NSString(string: NSUserName()),
|
||||
"manualStart": NSNumber(value: true),
|
||||
])
|
||||
return
|
||||
let options = try await prepareStartOptions()
|
||||
try manager.connection.startVPNTunnel(options: options)
|
||||
}
|
||||
#endif
|
||||
try manager.connection.startVPNTunnel(options: [
|
||||
|
||||
public func reloadService() async throws {
|
||||
let options = try await prepareStartOptions()
|
||||
let data = try ExtensionStartOptions.encode(options)
|
||||
guard let session = connection as? NETunnelProviderSession else {
|
||||
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",
|
||||
])
|
||||
}
|
||||
|
||||
public func fetchProfile() async {
|
||||
do {
|
||||
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)")
|
||||
}
|
||||
}
|
||||
|
||||
public func stop() async throws {
|
||||
@@ -130,7 +179,7 @@ public class ExtensionProfile: ObservableObject {
|
||||
do {
|
||||
try LibboxNewStandaloneCommandClient()!.serviceClose()
|
||||
} catch {
|
||||
NSLog("serviceClose error: \(error.localizedDescription)")
|
||||
logger.debug("serviceClose error: \(error.localizedDescription)")
|
||||
}
|
||||
manager.connection.stopVPNTunnel()
|
||||
}
|
||||
@@ -142,7 +191,7 @@ public class ExtensionProfile: ObservableObject {
|
||||
try await Task.sleep(nanoseconds: NSEC_PER_SEC)
|
||||
waitSeconds += 1
|
||||
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()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import Libbox
|
||||
import NetworkExtension
|
||||
import os.log
|
||||
#if os(iOS)
|
||||
import WidgetKit
|
||||
#endif
|
||||
@@ -9,20 +10,108 @@ import NetworkExtension
|
||||
#endif
|
||||
|
||||
open class ExtensionProvider: NEPacketTunnelProvider {
|
||||
public var username: String?
|
||||
private var commandServer: LibboxCommandServer!
|
||||
private static let logger = Logger(category: "ExtensionProvider")
|
||||
|
||||
public private(set) var commandServer: LibboxCommandServer?
|
||||
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()
|
||||
options.basePath = FilePath.sharedDirectory.relativePath
|
||||
options.workingPath = FilePath.workingDirectory.relativePath
|
||||
options.tempPath = FilePath.cacheDirectory.relativePath
|
||||
options.basePath = basePath
|
||||
options.workingPath = workingPath
|
||||
options.tempPath = tempPath
|
||||
|
||||
options.logMaxLines = 3000
|
||||
|
||||
#if os(tvOS)
|
||||
options.commandServerListenPort = await SharedPreferences.commandServerPort.get()
|
||||
options.commandServerSecret = await SharedPreferences.commandServerSecret.get()
|
||||
if let port = effectiveOptions?["commandServerPort"] as? NSNumber {
|
||||
options.commandServerListenPort = port.int32Value
|
||||
}
|
||||
if let secret = effectiveOptions?["commandServerSecret"] as? String {
|
||||
options.commandServerSecret = secret
|
||||
}
|
||||
#endif
|
||||
|
||||
var setupError: NSError?
|
||||
@@ -31,13 +120,8 @@ open class ExtensionProvider: NEPacketTunnelProvider {
|
||||
throw ExtensionStartupError("(packet-tunnel) error: setup service: \(setupError.localizedDescription)")
|
||||
}
|
||||
|
||||
var stderrError: NSError?
|
||||
LibboxRedirectStderr(FilePath.cacheDirectory.appendingPathComponent("stderr.log").relativePath, &stderrError)
|
||||
if let stderrError {
|
||||
throw ExtensionStartupError("(packet-tunnel) redirect stderr error: \(stderrError.localizedDescription)")
|
||||
}
|
||||
|
||||
await LibboxSetMemoryLimit(!SharedPreferences.ignoreMemoryLimit.get())
|
||||
let ignoreMemoryLimit = (effectiveOptions?["ignoreMemoryLimit"] as? NSNumber)?.boolValue ?? false
|
||||
LibboxSetMemoryLimit(!ignoreMemoryLimit)
|
||||
|
||||
if platformInterface == nil {
|
||||
platformInterface = ExtensionPlatformInterface(self)
|
||||
@@ -48,12 +132,31 @@ open class ExtensionProvider: NEPacketTunnelProvider {
|
||||
throw ExtensionStartupError("(packet-tunnel): create command server error: \(error.localizedDescription)")
|
||||
}
|
||||
do {
|
||||
try commandServer.start()
|
||||
try commandServer!.start()
|
||||
} catch {
|
||||
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")
|
||||
try await startService()
|
||||
#if os(macOS)
|
||||
if Variant.useSystemExtension {
|
||||
xpcService!.markServiceReady()
|
||||
}
|
||||
#endif
|
||||
#if os(iOS)
|
||||
if #available(iOS 18.0, *) {
|
||||
ControlCenter.shared.reloadControls(ofKind: ExtensionProfile.controlKind)
|
||||
@@ -68,48 +171,28 @@ open class ExtensionProvider: NEPacketTunnelProvider {
|
||||
}
|
||||
|
||||
private func startService() async throws {
|
||||
let profileID = await SharedPreferences.selectedProfileID.get()
|
||||
let profile: Profile?
|
||||
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)")
|
||||
guard let configContent = tunnelOptions?["configContent"] as? String else {
|
||||
throw ExtensionStartupError("(packet-tunnel) error: missing configContent in tunnel options")
|
||||
}
|
||||
|
||||
let options = LibboxOverrideOptions()
|
||||
do {
|
||||
try commandServer.startOrReloadService(configContent, options: options)
|
||||
try commandServer!.startOrReloadService(configContent, options: options)
|
||||
} catch {
|
||||
throw ExtensionStartupError("(packet-tunnel) error: start service: \(error.localizedDescription)")
|
||||
}
|
||||
#if os(macOS)
|
||||
await SharedPreferences.startedByUser.set(true)
|
||||
if commandServer.needWIFIState() {
|
||||
if !Variant.useSystemExtension {
|
||||
if !Variant.useSystemExtension, commandServer!.needWIFIState() {
|
||||
locationManager = CLLocationManager()
|
||||
locationDelegate = stubLocationDelegate()
|
||||
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
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
private var locationManager: CLLocationManager?
|
||||
private var locationDelegate: stubLocationDelegate?
|
||||
|
||||
class stubLocationDelegate: NSObject, CLLocationManagerDelegate {
|
||||
func locationManagerDidChangeAuthorization(_: CLLocationManager) {}
|
||||
|
||||
@@ -122,9 +205,9 @@ open class ExtensionProvider: NEPacketTunnelProvider {
|
||||
|
||||
func stopService() {
|
||||
do {
|
||||
try commandServer.closeService()
|
||||
try commandServer?.closeService()
|
||||
} catch {
|
||||
writeMessage("(packet-tunnel) error: stop service: \(error.localizedDescription)")
|
||||
writeMessage("(packet-tunnel) stop service: \(error.localizedDescription)")
|
||||
}
|
||||
if let platformInterface {
|
||||
platformInterface.reset()
|
||||
@@ -149,9 +232,15 @@ open class ExtensionProvider: NEPacketTunnelProvider {
|
||||
commandServer = nil
|
||||
}
|
||||
#if os(macOS)
|
||||
if reason == .userInitiated {
|
||||
await SharedPreferences.startedByUser.set(reason == .userInitiated)
|
||||
if Variant.useSystemExtension {
|
||||
xpcListener?.invalidate()
|
||||
xpcListener = nil
|
||||
xpcService?.commandServer = nil
|
||||
xpcService = nil
|
||||
UserServiceEndpointRegistry.shared.clear()
|
||||
}
|
||||
locationManager = nil
|
||||
locationDelegate = nil
|
||||
#endif
|
||||
#if os(iOS)
|
||||
if #available(iOS 18.0, *) {
|
||||
@@ -161,7 +250,15 @@ open class ExtensionProvider: NEPacketTunnelProvider {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,7 +1,10 @@
|
||||
#if os(macOS)
|
||||
import Foundation
|
||||
import os
|
||||
import SystemExtensions
|
||||
|
||||
private let logger = Logger(category: "SystemExtension")
|
||||
|
||||
public class SystemExtension: NSObject, OSSystemExtensionRequestDelegate {
|
||||
private let forceUpdate: Bool
|
||||
private let inBackground: Bool
|
||||
@@ -26,10 +29,10 @@
|
||||
existing.bundleVersion == ext.bundleVersion,
|
||||
existing.bundleShortVersion == ext.bundleShortVersion
|
||||
{
|
||||
NSLog("Skip update system extension")
|
||||
logger.info("Skip update system extension")
|
||||
return .cancel
|
||||
} else {
|
||||
NSLog("Update system extension")
|
||||
logger.info("Update system extension")
|
||||
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
|
||||
@@ -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
|
||||
@@ -15,6 +15,13 @@ public enum AppConfiguration {
|
||||
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 systemExtensionBundleID: String { "\(packageName).system" }
|
||||
public static var fileProviderDomainID: String { "\(packageName).workingdir" }
|
||||
@@ -22,4 +29,9 @@ public enum AppConfiguration {
|
||||
public static var profileUTType: String { "\(packageName).profile" }
|
||||
public static var backgroundTaskID: String { "\(packageName).update_profiles" }
|
||||
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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
import os
|
||||
|
||||
public extension Logger {
|
||||
init(category: String) {
|
||||
self.init(subsystem: Bundle.main.bundleIdentifier!, category: category)
|
||||
}
|
||||
}
|
||||
@@ -200,6 +200,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"App Settings" : {
|
||||
|
||||
},
|
||||
"Append `0.0.0.0/31` and `::/127` to `route_exclude_address` if not exists." : {
|
||||
"localizations" : {
|
||||
@@ -979,6 +982,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Enable" : {
|
||||
|
||||
},
|
||||
"enforceRoutes" : {
|
||||
"shouldTranslate" : false
|
||||
@@ -1168,6 +1174,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Helper Service" : {
|
||||
|
||||
},
|
||||
"Helper Service Required" : {
|
||||
|
||||
},
|
||||
"Hide VPN Icon" : {
|
||||
"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" : {
|
||||
"localizations" : {
|
||||
@@ -1364,6 +1379,9 @@
|
||||
},
|
||||
"includeAllNetworks" : {
|
||||
"shouldTranslate" : false
|
||||
},
|
||||
"Install" : {
|
||||
|
||||
},
|
||||
"Install Network Extension" : {
|
||||
"localizations" : {
|
||||
@@ -1530,6 +1548,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Managing working directory requires Helper Service." : {
|
||||
|
||||
},
|
||||
"Match Domains" : {
|
||||
"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." : {
|
||||
"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" : {
|
||||
"localizations" : {
|
||||
@@ -2508,6 +2535,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Unavailable" : {
|
||||
|
||||
},
|
||||
"Uninstall" : {
|
||||
"localizations" : {
|
||||
@@ -2631,6 +2661,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"WiFi State Access" : {
|
||||
|
||||
},
|
||||
"Working Directory" : {
|
||||
"localizations" : {
|
||||
|
||||
@@ -9,6 +9,8 @@ public struct MainView: View {
|
||||
@StateObject private var viewModel = MainViewModel()
|
||||
@State private var showCardManagement = false
|
||||
@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
|
||||
AnyView(CodeEditTextView(text: text, isEditable: isEditable))
|
||||
@@ -21,11 +23,12 @@ public struct MainView: View {
|
||||
SidebarView(selection: $viewModel.selection)
|
||||
.navigationSplitViewColumnWidth(150)
|
||||
} detail: {
|
||||
NavigationStack {
|
||||
NavigationStack(path: $settingsNavigationPath) {
|
||||
viewModel.selection.contentView
|
||||
.navigationTitle(viewModel.selection.title)
|
||||
}
|
||||
.environment(\.cardConfigurationVersion, cardConfigurationVersion)
|
||||
.environment(\.settingsNavigationPath, $settingsNavigationPath)
|
||||
.navigationSplitViewColumnWidth(650)
|
||||
}
|
||||
.frame(minHeight: 500)
|
||||
@@ -33,6 +36,7 @@ public struct MainView: View {
|
||||
viewModel.onAppear(environments: environments)
|
||||
}
|
||||
.alert($viewModel.alert)
|
||||
.globalChecks()
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigation) {
|
||||
StartStopButton()
|
||||
@@ -57,10 +61,32 @@ public struct MainView: View {
|
||||
}
|
||||
.onChangeCompat(of: viewModel.selection) { value in
|
||||
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) {
|
||||
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(\.importRemoteProfile, $viewModel.importRemoteProfile)
|
||||
.environment(\.profileEditor, profileEditor)
|
||||
|
||||
@@ -39,11 +39,8 @@ public class MainViewModel: BaseViewModel {
|
||||
if url.host == "import-remote-profile" {
|
||||
var error: NSError?
|
||||
importRemoteProfile = LibboxParseRemoteProfileImportLink(url.absoluteString, &error)
|
||||
if error != nil {
|
||||
return
|
||||
}
|
||||
if selection != .dashboard {
|
||||
selection = .dashboard
|
||||
if let error {
|
||||
alert = AlertState(error: error)
|
||||
}
|
||||
} else if url.pathExtension == "bpf" {
|
||||
Task {
|
||||
@@ -61,10 +58,6 @@ public class MainViewModel: BaseViewModel {
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
} catch {
|
||||
alert = AlertState(error: error)
|
||||
return
|
||||
}
|
||||
if selection != .dashboard {
|
||||
selection = .dashboard
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -192,16 +192,12 @@ public struct MenuView: View {
|
||||
environments.selectedProfileUpdate.send()
|
||||
if profile.status.isConnected {
|
||||
do {
|
||||
try await serviceReload()
|
||||
try await profile.reloadService()
|
||||
} catch {
|
||||
alert = AlertState(error: error)
|
||||
}
|
||||
}
|
||||
reasserting = false
|
||||
}
|
||||
|
||||
private nonisolated func serviceReload() async throws {
|
||||
try LibboxNewStandaloneCommandClient()!.serviceReload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ archive_ios:
|
||||
upload_ios:
|
||||
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:
|
||||
xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e " ARCHIVE FAILED" -e "❌"
|
||||
|
||||
+1
-8
@@ -123,6 +123,7 @@ struct MainView: View {
|
||||
environments.postReload()
|
||||
}
|
||||
.alert($alert)
|
||||
.globalChecks()
|
||||
.onChangeCompat(of: scenePhase) { newValue in
|
||||
if newValue == .active {
|
||||
environments.postReload()
|
||||
@@ -185,10 +186,6 @@ struct MainView: View {
|
||||
importRemoteProfile = LibboxParseRemoteProfileImportLink(url.absoluteString, &error)
|
||||
if let error {
|
||||
alert = AlertState(error: error)
|
||||
return
|
||||
}
|
||||
if selection != .dashboard {
|
||||
selection = .dashboard
|
||||
}
|
||||
} else if url.pathExtension == "bpf" {
|
||||
do {
|
||||
@@ -197,10 +194,6 @@ struct MainView: View {
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
} catch {
|
||||
alert = AlertState(error: error)
|
||||
return
|
||||
}
|
||||
if selection != .dashboard {
|
||||
selection = .dashboard
|
||||
}
|
||||
} else {
|
||||
alert = AlertState(errorMessage: String(localized: "Handled unknown URL \(url.absoluteString)"))
|
||||
|
||||
@@ -113,5 +113,13 @@
|
||||
<string>As a universal proxy platform, sing-box configures routing according to your configuration.</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<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>
|
||||
</plist>
|
||||
|
||||
@@ -20,17 +20,13 @@
|
||||
<array>
|
||||
<string>iCloud.io.nekohasekai.sfavt</string>
|
||||
</array>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>$(TeamIdentifierPrefix)$(BASE_PACKAGE_IDENTIFIER)</string>
|
||||
</array>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<key>com.apple.developer.networking.wifi-info</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<key>com.apple.security.personal-information.location</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
import Libbox
|
||||
import Library
|
||||
import MacLibrary
|
||||
|
||||
@@ -7,6 +8,8 @@ class StandaloneApplicationDelegate: ApplicationDelegate {
|
||||
func applicationWillFinishLaunching(_: Notification) {
|
||||
Variant.useSystemExtension = true
|
||||
Variant.isBeta = false
|
||||
LibboxSetXPCDialer(CommandXPCDialer.shared)
|
||||
UserServiceEndpointPublisher.shared.start()
|
||||
Task {
|
||||
await setupSystemExtension()
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ struct MainView: View {
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
@EnvironmentObject private var environments: ExtensionEnvironments
|
||||
@State private var selection = NavigationPage.dashboard
|
||||
@State private var importProfile: LibboxProfileContent?
|
||||
@State private var importRemoteProfile: LibboxImportRemoteProfile?
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $selection) {
|
||||
@@ -32,6 +34,9 @@ struct MainView: View {
|
||||
environments.connect()
|
||||
}
|
||||
}
|
||||
.globalChecks()
|
||||
.environment(\.selection, $selection)
|
||||
.environment(\.importProfile, $importProfile)
|
||||
.environment(\.importRemoteProfile, $importRemoteProfile)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,4 @@
|
||||
import Libbox
|
||||
import Library
|
||||
import NetworkExtension
|
||||
import System
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
class PacketTunnelProvider: ExtensionProvider {}
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
<array>
|
||||
<string>packet-tunnel-provider-systemextension</string>
|
||||
</array>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>$(TeamIdentifierPrefix)$(BASE_PACKAGE_IDENTIFIER)</string>
|
||||
</array>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -42,6 +42,9 @@
|
||||
3AE396072C21A60C00647718 /* 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, ); }; };
|
||||
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, ); }; };
|
||||
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, ); }; };
|
||||
@@ -183,6 +186,20 @@
|
||||
remoteGlobalIDString = 3AEC211C2A459B4700A63465;
|
||||
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 */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 3AEC20BD2A45991900A63465 /* Project object */;
|
||||
@@ -343,6 +360,37 @@
|
||||
name = "Embed Frameworks";
|
||||
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 */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -384,6 +432,11 @@
|
||||
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; };
|
||||
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>"; };
|
||||
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; };
|
||||
@@ -545,6 +598,7 @@
|
||||
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>"; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -611,6 +665,14 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
3AE5956E2F08C34000C13426 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3AE595822F08C3D400C13426 /* Library.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
3AEC20F02A459AB400A63465 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -676,6 +738,15 @@
|
||||
/* End PBXFrameworksBuildPhase 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 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -694,8 +765,10 @@
|
||||
3ADDCFCD2E8B72B8009ACE1D /* TVExtension */,
|
||||
3ADDCFD52E8B72BC009ACE1D /* WidgetExtension */,
|
||||
3AAAFB232EF5218F004C69AD /* FileProviderExtension */,
|
||||
3AE595722F08C34000C13426 /* HelperService */,
|
||||
3AEC20C72A45991900A63465 /* Products */,
|
||||
3AEC21012A459AE300A63465 /* Frameworks */,
|
||||
3AE5C9042F08E53400C13426 /* Recovered References */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -715,6 +788,7 @@
|
||||
3AE171992A8128DD00393060 /* TVExtension.appex */,
|
||||
3AE395F22C21A5CA00647718 /* WidgetExtension.appex */,
|
||||
3AAAFB202EF5218F004C69AD /* FileProviderExtension.appex */,
|
||||
3AE595712F08C34000C13426 /* RootHelper */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -722,6 +796,8 @@
|
||||
3AEC21012A459AE300A63465 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3AE595A32F08C4ED00C13426 /* UserNotifications.framework */,
|
||||
3AE595A12F08C4E900C13426 /* CoreWLAN.framework */,
|
||||
3AAAFE162EF52772004C69AD /* FileProvider.framework */,
|
||||
3AC72A2E2EED94DD0039DEA4 /* SystemConfiguration.framework */,
|
||||
3AC72A302EED94F60039DEA4 /* SystemConfiguration.framework */,
|
||||
@@ -909,6 +985,29 @@
|
||||
productReference = 3AE395F22C21A5CA00647718 /* WidgetExtension.appex */;
|
||||
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 */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 3AEC20FE2A459AB500A63465 /* Build configuration list for PBXNativeTarget "SFI" */;
|
||||
@@ -1023,10 +1122,13 @@
|
||||
3AEECC022A6DF9CA006A0E0C /* Resources */,
|
||||
3AEECC212A6DFA79006A0E0C /* Embed Frameworks */,
|
||||
3AEECC232A6DFA79006A0E0C /* Embed System Extensions */,
|
||||
3AE595A92F08C5E200C13426 /* Copy Helpers */,
|
||||
3AE595AC2F08C63000C13426 /* Copy LaunchDaemon */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
3AE595A62F08C5C700C13426 /* PBXTargetDependency */,
|
||||
3AEECC1C2A6DFA79006A0E0C /* PBXTargetDependency */,
|
||||
3AEECC202A6DFA79006A0E0C /* PBXTargetDependency */,
|
||||
3AEECC3D2A6DFDC6006A0E0C /* PBXTargetDependency */,
|
||||
@@ -1097,6 +1199,9 @@
|
||||
3AE395F12C21A5CA00647718 = {
|
||||
CreatedOnToolsVersion = 16.0;
|
||||
};
|
||||
3AE595702F08C34000C13426 = {
|
||||
CreatedOnToolsVersion = 26.2;
|
||||
};
|
||||
3AEC20F22A459AB400A63465 = {
|
||||
CreatedOnToolsVersion = 15.0;
|
||||
};
|
||||
@@ -1155,6 +1260,7 @@
|
||||
3A77016C2A4E6B34008F031F /* IntentsExtension */,
|
||||
3AE395F12C21A5CA00647718 /* WidgetExtension */,
|
||||
3AAAFB1F2EF5218F004C69AD /* FileProviderExtension */,
|
||||
3AE595702F08C34000C13426 /* RootHelper */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -1296,6 +1402,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
3AE5956D2F08C34000C13426 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
3AEC20EF2A459AB400A63465 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -1426,6 +1539,16 @@
|
||||
target = 3AEC211C2A459B4700A63465 /* Library */;
|
||||
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 */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 3A096F852A4ED3DE00D4A2ED /* Extension */;
|
||||
@@ -1980,6 +2103,86 @@
|
||||
};
|
||||
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 */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
@@ -2379,7 +2582,18 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = 287TTNZF8L;
|
||||
ENABLE_APP_SANDBOX = 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;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = SystemExtension/Info.plist;
|
||||
@@ -2416,7 +2630,18 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = 287TTNZF8L;
|
||||
ENABLE_APP_SANDBOX = 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;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = SystemExtension/Info.plist;
|
||||
@@ -2669,6 +2894,15 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
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" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
||||
Reference in New Issue
Block a user