mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-10-27 20:53:44 +00:00
Implements issue #71 with ADR-0020 (supersedes ADR-0019's endpoint-signature cleanup, startup cleanup, and health-gate cleanup). - New ShadowsocksX-NG2SystemProxyHelper target: app-bundled root LaunchDaemon (SMAppService.daemon, on-demand via MachServices) that mechanically applies a closed typed configuration or executes an unconditional clear. Clients are accepted by code-signing bundle identifier only; requests run serially on a shared queue (last-writer-wins). - SystemProxyConfiguration is now the closed typed apply payload (Codable over JSON XPC): explicit SOCKS/HTTP/HTTPS enable + endpoints, PAC and auto-discovery disabled with URL/JS removed, explicit simple-hostname exclusion, complete ExceptionsList. Unmodeled Proxies keys are preserved and equivalent values skip the commit. - Cleanup is a side effect of a persisted system-proxy intent on-to-off transition only: the switch itself, or the agent-off cascade (persist both intents off, clear, then stop listeners; failed cascade persistence records no transition and clears nothing). Launch with the switch off, health-gate closure, invalid targets, and passive events never clear. Failed clears are surfaced without deferred retry. - Endpoint-signature store, ownership migration, and direct Authorization Services writes are removed; old signature files stay inert. Helper unavailability or pending approval keeps an enabled intent pending and surfaces the Login Items approval path (HomeView button + menu label).
182 lines
6.9 KiB
Swift
182 lines
6.9 KiB
Swift
import Foundation
|
|
import SystemConfiguration
|
|
|
|
/// root 进程内的 SystemConfiguration 写入器(issue #71):GUI 域决策的机械
|
|
/// 执行者。apply 覆盖活动网络位置的每个服务,只改 typed 字段并保留未建模
|
|
/// 键,值已等价时跳过提交;clear 无条件移除全部位置全部服务的完整 Proxies
|
|
/// 字典。root 身份直接创建 SCPreferences,不走授权对话框。
|
|
enum SystemProxyWriter {
|
|
static func perform(
|
|
_ request: SystemProxyHelperEngine.Request
|
|
) throws -> SystemProxyHelperEngine.Outcome {
|
|
switch request {
|
|
case .apply(let configuration):
|
|
return .applied(try apply(configuration))
|
|
case .clear:
|
|
try clear()
|
|
return .cleared
|
|
}
|
|
}
|
|
|
|
static func apply(_ configuration: SystemProxyConfiguration) throws -> SystemProxyWriteOutcome {
|
|
try withPreferences { preferences in
|
|
try lockPreferencesOrThrow(preferences)
|
|
defer { SCPreferencesUnlock(preferences) }
|
|
|
|
let services = try activeServiceSnapshots(in: preferences)
|
|
guard !services.isEmpty else { throw SystemProxyError.noProxyServices }
|
|
let plan = try SystemProxyPlanner.makeApplyPlan(
|
|
services: services.map(\.plannerState), configuration: configuration)
|
|
|
|
guard !plan.writes.isEmpty else { return .unchanged }
|
|
let servicesByID = Dictionary(uniqueKeysWithValues: services.map { ($0.identifier, $0) })
|
|
for write in plan.writes {
|
|
guard let service = servicesByID[write.identifier] else {
|
|
throw SystemProxyError.cannotWriteService(write.identifier.serviceID)
|
|
}
|
|
if service.proxyProtocol == nil {
|
|
_ = SCNetworkServiceAddProtocolType(
|
|
service.networkService, kSCNetworkProtocolTypeProxies)
|
|
}
|
|
guard
|
|
let proxyProtocol = service.proxyProtocol
|
|
?? SCNetworkServiceCopyProtocol(service.networkService, kSCNetworkProtocolTypeProxies),
|
|
setConfiguration(write.configuration, on: proxyProtocol)
|
|
else { throw SystemProxyError.cannotWriteService(write.identifier.serviceID) }
|
|
}
|
|
try commitAndApply(preferences)
|
|
return .written
|
|
}
|
|
}
|
|
|
|
static func clear() throws {
|
|
try withPreferences { preferences in
|
|
try lockPreferencesOrThrow(preferences)
|
|
defer { SCPreferencesUnlock(preferences) }
|
|
|
|
let services = try allServiceSnapshots(in: preferences)
|
|
let plan = SystemProxyPlanner.makeClearPlan(services: services.map(\.plannerState))
|
|
guard !plan.writes.isEmpty else { return }
|
|
|
|
let servicesByID = Dictionary(uniqueKeysWithValues: services.map { ($0.identifier, $0) })
|
|
for write in plan.writes {
|
|
guard let service = servicesByID[write.identifier],
|
|
let proxyProtocol = service.proxyProtocol,
|
|
setConfiguration(nil, on: proxyProtocol)
|
|
else { throw SystemProxyError.cannotWriteService(write.identifier.serviceID) }
|
|
}
|
|
try commitAndApply(preferences)
|
|
}
|
|
}
|
|
|
|
// MARK: - SystemConfiguration 读取与写入
|
|
|
|
private struct ServiceSnapshot {
|
|
let identifier: SystemProxyServiceIdentifier
|
|
let networkService: SCNetworkService
|
|
let proxyProtocol: SCNetworkProtocol?
|
|
let configuration: Data?
|
|
|
|
var plannerState: SystemProxyServiceState {
|
|
SystemProxyServiceState(identifier: identifier, configuration: configuration)
|
|
}
|
|
}
|
|
|
|
private static func withPreferences<T>(_ body: (SCPreferences) throws -> T) throws -> T {
|
|
guard
|
|
let preferences = SCPreferencesCreate(
|
|
nil, SystemProxyHelperIdentity.machServiceName as CFString, nil)
|
|
else { throw SystemProxyError.preferencesUnavailable }
|
|
return try body(preferences)
|
|
}
|
|
|
|
private static func activeServiceSnapshots(
|
|
in preferences: SCPreferences
|
|
) throws -> [ServiceSnapshot] {
|
|
guard let set = SCNetworkSetCopyCurrent(preferences) else {
|
|
throw SystemProxyError.noCurrentNetworkSet
|
|
}
|
|
guard let locationID = SCNetworkSetGetSetID(set) as String? else {
|
|
throw SystemProxyError.noCurrentNetworkSet
|
|
}
|
|
return try serviceSnapshots(in: set, locationID: locationID)
|
|
}
|
|
|
|
private static func allServiceSnapshots(
|
|
in preferences: SCPreferences
|
|
) throws -> [ServiceSnapshot] {
|
|
guard let sets = SCNetworkSetCopyAll(preferences) as? [SCNetworkSet], !sets.isEmpty else {
|
|
throw SystemProxyError.noNetworkLocations
|
|
}
|
|
return try sets.flatMap { set -> [ServiceSnapshot] in
|
|
guard let locationID = SCNetworkSetGetSetID(set) as String? else {
|
|
return [ServiceSnapshot]()
|
|
}
|
|
return try serviceSnapshots(in: set, locationID: locationID)
|
|
}
|
|
}
|
|
|
|
private static func serviceSnapshots(
|
|
in set: SCNetworkSet, locationID: String
|
|
) throws -> [ServiceSnapshot] {
|
|
guard let services = SCNetworkSetCopyServices(set) as? [SCNetworkService] else {
|
|
return []
|
|
}
|
|
return try services.compactMap { service in
|
|
guard let serviceID = SCNetworkServiceGetServiceID(service) as String? else { return nil }
|
|
let proxyProtocol = SCNetworkServiceCopyProtocol(service, kSCNetworkProtocolTypeProxies)
|
|
let configurationValue: CFPropertyList?
|
|
if let proxyProtocol {
|
|
configurationValue = SCNetworkProtocolGetConfiguration(proxyProtocol)
|
|
} else {
|
|
configurationValue = nil
|
|
}
|
|
return ServiceSnapshot(
|
|
identifier: SystemProxyServiceIdentifier(
|
|
locationID: locationID, serviceID: serviceID),
|
|
networkService: service,
|
|
proxyProtocol: proxyProtocol,
|
|
configuration: try propertyListData(from: configurationValue, serviceID: serviceID))
|
|
}
|
|
}
|
|
|
|
private static func propertyListData(
|
|
from value: CFPropertyList?, serviceID: String
|
|
) throws -> Data? {
|
|
guard let value else { return nil }
|
|
guard let dictionary = value as? [String: Any] else {
|
|
throw SystemProxyError.unreadableService(serviceID)
|
|
}
|
|
return try SystemProxyPlanner.propertyListData(from: dictionary, serviceID: serviceID)
|
|
}
|
|
|
|
private static func setConfiguration(_ data: Data?, on proxyProtocol: SCNetworkProtocol) -> Bool {
|
|
guard let data else { return SCNetworkProtocolSetConfiguration(proxyProtocol, nil) }
|
|
guard
|
|
let propertyList = try? PropertyListSerialization.propertyList(
|
|
from: data, options: [], format: nil),
|
|
let dictionary = propertyList as? [String: Any]
|
|
else { return false }
|
|
return SCNetworkProtocolSetConfiguration(proxyProtocol, dictionary as CFDictionary)
|
|
}
|
|
|
|
private static func commitAndApply(_ preferences: SCPreferences) throws {
|
|
guard SCPreferencesCommitChanges(preferences) else {
|
|
throw SystemProxyError.commitFailed(systemConfigurationError())
|
|
}
|
|
guard SCPreferencesApplyChanges(preferences) else {
|
|
throw SystemProxyError.applyFailed(systemConfigurationError())
|
|
}
|
|
}
|
|
|
|
private static func systemConfigurationError() -> String {
|
|
String(cString: SCErrorString(SCError()))
|
|
}
|
|
}
|
|
|
|
private func lockPreferencesOrThrow(_ preferences: SCPreferences) throws {
|
|
guard SCPreferencesLock(preferences, true) else {
|
|
throw SystemProxyError.preferencesBusy
|
|
}
|
|
}
|