diff --git a/ApplicationLibrary/Views/Setting/MacAppView.swift b/ApplicationLibrary/Views/Setting/MacAppView.swift index ddc8513..c5015ad 100644 --- a/ApplicationLibrary/Views/Setting/MacAppView.swift +++ b/ApplicationLibrary/Views/Setting/MacAppView.swift @@ -76,9 +76,15 @@ public struct AppView: View { Section { if rootHelperRegistrationStatus == .enabled { FormButton { - performHelperAction { - try HelperServiceManager.unregisterRootHelper() - try HelperServiceManager.registerRootHelper() + Task { + do { + try HelperServiceManager.unregisterRootHelper() + try await Task.sleep(for: .seconds(1)) + try HelperServiceManager.registerRootHelper() + refreshHelperStatus() + } catch { + alert = AlertState(error: error) + } } } label: { Label("Update", systemImage: "arrow.down.doc.fill") diff --git a/HelperService/RootHelperService.swift b/HelperService/RootHelperService.swift index 39c6622..2069c64 100644 --- a/HelperService/RootHelperService.swift +++ b/HelperService/RootHelperService.swift @@ -8,46 +8,22 @@ 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 = NSXPCListener(machServiceName: AppConfiguration.rootHelperMachService) 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")") + let systemExtensionID = AppConfiguration.systemExtensionBundleID + let standaloneID = AppConfiguration.packageName + ".standalone" + let teamID = AppConfiguration.teamID + let requirement = "(identifier \"\(systemExtensionID)\" or identifier \"\(standaloneID)\") and anchor apple generic and certificate leaf[subject.OU] = \"\(teamID)\"" + do { + try newConnection.setCodeSigningRequirement(requirement) + } catch { + logger.warning("Rejected XPC connection: \(error.localizedDescription)") return false } @@ -106,4 +82,8 @@ extension RootHelperService: RootHelperProtocol { reply(error as NSError) } } + + func getVersion(reply: @escaping (String) -> Void) { + reply(Bundle.main.version) + } } diff --git a/Library/Network/CommandXPC.swift b/Library/Network/CommandXPC.swift index 54d2347..2a7da4d 100644 --- a/Library/Network/CommandXPC.swift +++ b/Library/Network/CommandXPC.swift @@ -53,14 +53,12 @@ } 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")") + let bundleID = AppConfiguration.packageName + ".standalone" + let requirement = "identifier \"\(bundleID)\" and anchor apple generic and certificate leaf[subject.OU] = \"\(AppConfiguration.teamID)\"" + do { + try newConnection.setCodeSigningRequirement(requirement) + } catch { + logger.warning("Rejected XPC connection: \(error.localizedDescription)") return false } diff --git a/Library/Network/HelperServiceManager.swift b/Library/Network/HelperServiceManager.swift index 0c8d3ea..0ca650f 100644 --- a/Library/Network/HelperServiceManager.swift +++ b/Library/Network/HelperServiceManager.swift @@ -1,7 +1,10 @@ #if os(macOS) import Foundation + import os import ServiceManagement + private let logger = Logger(category: "HelperServiceManager") + public enum HelperServiceManager { private static var rootHelperService: SMAppService { SMAppService.daemon(plistName: "\(AppConfiguration.rootHelperBundleID).plist") @@ -21,5 +24,25 @@ public static func unregisterRootHelper() throws { try rootHelperService.unregister() } + + public static func updateRootHelperIfNeeded() async { + guard rootHelperStatus == .enabled else { return } + + do { + let installedVersion = try RootHelperClient.shared.getVersion() + let currentVersion = Bundle.main.version + guard currentVersion != installedVersion else { return } + } catch { + logger.warning("Failed to get root helper version, updating: \(error.localizedDescription)") + } + + do { + try unregisterRootHelper() + try await Task.sleep(for: .seconds(1)) + try registerRootHelper() + } catch { + logger.error("Failed to update root helper: \(error.localizedDescription)") + } + } } #endif diff --git a/Library/Network/RootHelperXPC.swift b/Library/Network/RootHelperXPC.swift index bc96ea5..f87eca8 100644 --- a/Library/Network/RootHelperXPC.swift +++ b/Library/Network/RootHelperXPC.swift @@ -42,6 +42,7 @@ func getWorkingDirectorySize(reply: @escaping (Int64, NSError?) -> Void) func cleanWorkingDirectory(reply: @escaping (NSError?) -> Void) + func getVersion(reply: @escaping (String) -> Void) } public enum RootHelperXPC { @@ -217,5 +218,52 @@ proxy.cleanWorkingDirectory(reply: reply) } } + + public func getVersion() throws -> String { + let semaphore = DispatchSemaphore(value: 0) + var result: String? + var resultError: NSError? + + let conn = getConnection() + guard let proxy = conn.remoteObjectProxyWithErrorHandler({ error in + logger.error("getVersion 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", + ]) + } + + proxy.getVersion { version in + result = version + semaphore.signal() + } + + let timeout = DispatchTime.now() + .seconds(5) + if semaphore.wait(timeout: timeout) == .timedOut { + let error = NSError(domain: "RootHelper", code: -1, userInfo: [ + NSLocalizedDescriptionKey: "getVersion request timeout", + ]) + logger.error("getVersion: timeout") + throw error + } + + if let error = resultError { + throw error + } + + guard let value = result else { + throw NSError(domain: "RootHelper", code: -1, userInfo: [ + NSLocalizedDescriptionKey: "getVersion returned nil", + ]) + } + + return value + } } #endif diff --git a/Library/Network/UserServiceEndpointPublisher.swift b/Library/Network/UserServiceEndpointPublisher.swift index 3b6a420..2fa8649 100644 --- a/Library/Network/UserServiceEndpointPublisher.swift +++ b/Library/Network/UserServiceEndpointPublisher.swift @@ -46,14 +46,12 @@ } 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")") + let bundleID = AppConfiguration.systemExtensionBundleID + let requirement = "identifier \"\(bundleID)\" and anchor apple generic and certificate leaf[subject.OU] = \"\(AppConfiguration.teamID)\"" + do { + try newConnection.setCodeSigningRequirement(requirement) + } catch { + logger.warning("Rejected XPC connection: \(error.localizedDescription)") return false } diff --git a/Library/Network/XPCConnectionValidator.swift b/Library/Network/XPCConnectionValidator.swift deleted file mode 100644 index 6b07c40..0000000 --- a/Library/Network/XPCConnectionValidator.swift +++ /dev/null @@ -1,80 +0,0 @@ -#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 diff --git a/Library/Shared/Bundle+Version.swift b/Library/Shared/Bundle+Version.swift index d527e0f..577ab96 100644 --- a/Library/Shared/Bundle+Version.swift +++ b/Library/Shared/Bundle+Version.swift @@ -1,6 +1,6 @@ import Foundation -extension Bundle { +public extension Bundle { var version: String { infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown" } diff --git a/Localizable.xcstrings b/Localizable.xcstrings index ff9ae31..949c735 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -202,7 +202,14 @@ } }, "App Settings" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "应用设置" + } + } + } }, "Append `0.0.0.0/31` and `::/127` to `route_exclude_address` if not exists." : { "localizations" : { @@ -562,6 +569,16 @@ } } }, + "Context destroyed" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "上下文已销毁" + } + } + } + }, "Copy" : { "localizations" : { "zh-Hans" : { @@ -984,7 +1001,14 @@ } }, "Enable" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "启用" + } + } + } }, "enforceRoutes" : { "shouldTranslate" : false @@ -1068,6 +1092,16 @@ } } }, + "Failed to save rules" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "保存规则失败" + } + } + } + }, "false" : { "localizations" : { "zh-Hans" : { @@ -1176,10 +1210,24 @@ } }, "Helper Service" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "辅助服务" + } + } + } }, "Helper Service Required" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "需要辅助服务" + } + } + } }, "Hide VPN Icon" : { "localizations" : { @@ -1355,7 +1403,14 @@ } }, "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." : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在 SFM 独立版本中,读取 WiFi 状态需要应用保持运行。退出 SFM 应用后,sing-box 服务将无法继续提供 `wifi_ssid` 和 `wifi_bssid` 路由规则。" + } + } + } }, "Inbound" : { "localizations" : { @@ -1381,7 +1436,14 @@ "shouldTranslate" : false }, "Install" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "安装" + } + } + } }, "Install Network Extension" : { "localizations" : { @@ -1424,6 +1486,7 @@ } }, "Invalid QR Code" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1550,7 +1613,14 @@ } }, "Managing working directory requires Helper Service." : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "管理工作目录需要辅助服务。" + } + } + } }, "Match Domains" : { "localizations" : { @@ -1585,6 +1655,26 @@ } } }, + "Missing access to selected file" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法访问所选文件" + } + } + } + }, + "Missing file" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "缺少文件" + } + } + } + }, "Missing path" : { "localizations" : { "zh-Hans" : { @@ -1897,6 +1987,26 @@ } } }, + "Profile missing" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "配置丢失" + } + } + } + }, + "Profile not found" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未找到配置" + } + } + } + }, "Profile Override" : { "localizations" : { "zh-Hans" : { @@ -2075,6 +2185,7 @@ } }, "Scanner Error" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -2115,6 +2226,7 @@ } }, "Service Error" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -2451,7 +2563,14 @@ } }, "The sing-box service requires Helper Service to provide process lookup functionality, which supports `process_name` and `process_path` routing rules." : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "sing-box 服务需要辅助服务来提供进程查找功能,以支持 `process_name` 和 `process_path` 路由规则。" + } + } + } }, "This app needs to be placed under the Applications folder to work." : { "localizations" : { @@ -2464,7 +2583,14 @@ } }, "This helper service provides process lookup for `process_name` and `process_path` routing rules, and manages the working directory." : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此辅助服务为 `process_name` 和 `process_path` 路由规则提供进程查找功能,并管理工作目录。" + } + } + } }, "To Clipboard" : { "localizations" : { @@ -2537,7 +2663,24 @@ } }, "Unavailable" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "不可用" + } + } + } + }, + "Unexpected message type %lld" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "意外的消息类型 %lld" + } + } + } }, "Uninstall" : { "localizations" : { @@ -2570,6 +2713,7 @@ } }, "Update Failed" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -2663,7 +2807,14 @@ } }, "WiFi State Access" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "WiFi 状态访问" + } + } + } }, "Working Directory" : { "localizations" : { diff --git a/Makefile b/Makefile index e476a0b..68dffea 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,6 @@ +SHELL := /bin/bash +.SHELLFLAGS := -o pipefail -c + build_all: build_ios build_macos build_tvos build_ios: diff --git a/SFM.System/StandaloneApplicationDelegate.swift b/SFM.System/StandaloneApplicationDelegate.swift index b05c660..d3cbf54 100644 --- a/SFM.System/StandaloneApplicationDelegate.swift +++ b/SFM.System/StandaloneApplicationDelegate.swift @@ -12,6 +12,7 @@ class StandaloneApplicationDelegate: ApplicationDelegate { UserServiceEndpointPublisher.shared.start() Task { await setupSystemExtension() + await HelperServiceManager.updateRootHelperIfNeeded() } } diff --git a/sing-box.xcodeproj/project.pbxproj b/sing-box.xcodeproj/project.pbxproj index ea60ccd..093f076 100644 --- a/sing-box.xcodeproj/project.pbxproj +++ b/sing-box.xcodeproj/project.pbxproj @@ -1836,6 +1836,7 @@ 3AAAFB2F2EF5218F004C69AD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + APP_GROUP_IDENTIFIER = "group.$(BASE_PACKAGE_IDENTIFIER)"; CODE_SIGN_ENTITLEMENTS = FileProviderExtension/FileProviderExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; @@ -1868,6 +1869,7 @@ 3AAAFB302EF5218F004C69AD /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + APP_GROUP_IDENTIFIER = "group.$(BASE_PACKAGE_IDENTIFIER)"; CODE_SIGN_ENTITLEMENTS = FileProviderExtension/FileProviderExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; @@ -2127,6 +2129,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = "1.13.0-alpha.21"; PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_PACKAGE_IDENTIFIER).helper"; PRODUCT_NAME = "$(TARGET_NAME)"; RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO; @@ -2167,6 +2170,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = "1.13.0-alpha.21"; PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_PACKAGE_IDENTIFIER).helper"; PRODUCT_NAME = "$(TARGET_NAME)"; RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO; @@ -2513,6 +2517,7 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = macosx; SKIP_INSTALL = YES; + STRIP_STYLE = "non-global"; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -2536,7 +2541,6 @@ CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; DEFINES_MODULE = YES; - STRIP_STYLE = "non-global"; DEVELOPMENT_TEAM = 287TTNZF8L; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -2561,6 +2565,7 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = macosx; SKIP_INSTALL = YES; + STRIP_STYLE = "non-global"; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;