Files
ShadowsocksX-NG/ShadowsocksX-NG2/Domain/SystemProxyHelperEngine.swift
T
Qiu Yuzhou d2a8d6105c feat(system-proxy): route writes through privileged LaunchDaemon helper
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).
2026-09-30 12:26:40 +08:00

75 lines
2.4 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
/// 特权 helper 的请求引擎(issue #71):解码 closed typed 请求 → 注入的特权
/// 写入器 → 编码应答。引擎本身不含任何产品策略,只在 payload 无效时拒绝。
/// 所有连接共享一条串行队列:请求逐个执行,最后接受的请求决定设备级配置
/// (last-writer-wins)。
final class SystemProxyHelperEngine: NSObject, SystemProxyHelperControlling {
/// 注入的特权写入器:真正触到 SystemConfiguration 的唯一边界。
typealias Performing =
@Sendable (SystemProxyHelperEngine.Request) throws ->
SystemProxyHelperEngine.Outcome
enum Request: Equatable, Sendable {
case apply(SystemProxyConfiguration)
case clear
}
enum Outcome: Equatable, Sendable {
case applied(SystemProxyWriteOutcome)
case cleared
}
/// 跨全部 XPC 连接共享的串行执行队列。
private static let sharedQueue = DispatchQueue(
label: "\(SystemProxyHelperIdentity.machServiceName).work")
private let queue: DispatchQueue
private let perform: Performing
init(perform: @escaping Performing, queue: DispatchQueue? = nil) {
self.perform = perform
self.queue = queue ?? Self.sharedQueue
super.init()
}
func apply(_ payload: Data, withReply reply: @escaping (Data) -> Void) {
queue.async { [perform] in
let request = Result { try SystemProxyHelperWire.decodeConfiguration(payload) }
reply(Self.responseData(request: request.map { .apply($0) }, perform: perform))
}
}
func clear(withReply reply: @escaping (Data) -> Void) {
queue.async { [perform] in
reply(Self.responseData(request: .success(.clear), perform: perform))
}
}
// MARK: - 请求执行(纯逻辑,可测)
static func responseData(
request: Result<Request, Error>, perform: Performing
) -> Data {
SystemProxyHelperWire.encode(response(request: request, perform: perform))
}
static func response(
request: Result<Request, Error>, perform: Performing
) -> SystemProxyHelperResponse {
do {
let outcome = try perform(request.get())
switch outcome {
case .applied(let writeOutcome):
return .applied(writeOutcome)
case .cleared:
return .cleared
}
} catch let error as SystemProxyError {
return .failure(error)
} catch {
return .failure(.helperUnavailable(String(describing: error)))
}
}
}