mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-15 02:37:32 +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).
67 lines
3.1 KiB
Swift
67 lines
3.1 KiB
Swift
import Foundation
|
||
|
||
/// 跨进程运行时路径契约(spec #21 D5,issue #27):GUI 写、wrapper 读的固定
|
||
/// 位置。目录 0700、文件 0600,由写入方与 wrapper 共同维护。
|
||
enum RuntimePaths {
|
||
static func runtimeDirectory() -> URL {
|
||
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
||
.appendingPathComponent("ShadowsocksX-NG2", isDirectory: true)
|
||
}
|
||
|
||
/// sslocal 配置契约:激活派生的完整 JSON,GUI 原子写入、wrapper 以绝对路径
|
||
/// 交给 `sslocal -c`(也是 SIGUSR1 热重载时上游重读的文件)。
|
||
static func runtimeFileURL() -> URL {
|
||
runtimeDirectory().appendingPathComponent("sslocal-active.json")
|
||
}
|
||
|
||
/// ACL 稳定链接(ADR-0011):sslocal 经契约 `acl` 键读取;指向某个
|
||
/// `acl-<summary>.ini` 变体,换模式只改链接指向。
|
||
static func aclFileURL() -> URL {
|
||
runtimeDirectory().appendingPathComponent("acl-active.ini")
|
||
}
|
||
|
||
/// wrapper 进程 pid 文件:SMAppService.Status 不暴露运行中 agent 的 pid,
|
||
/// wrapper 自写此文件供 GUI 判活(kill(pid, 0))并投递 SIGUSR1。
|
||
static func agentPIDFileURL() -> URL {
|
||
runtimeDirectory().appendingPathComponent("agent.pid")
|
||
}
|
||
|
||
/// The wrapper's sslocal deployment receipt. ACL-backed receipts are written
|
||
/// after the child owns the configured TCP listeners; the GUI checks its
|
||
/// digest and PID during direct-mode health checks.
|
||
static func agentRuntimeStatusURL() -> URL {
|
||
runtimeDirectory().appendingPathComponent("agent-runtime-status.json")
|
||
}
|
||
|
||
/// wrapper 与 sslocal 的收敛日志(stderr 重定向目标);滚动与诊断导出由 #33
|
||
/// 完善。不属于「运行时文件」,显式停止时不删除。
|
||
static func agentLogURL() -> URL {
|
||
runtimeDirectory().appendingPathComponent("agent.log")
|
||
}
|
||
|
||
// 旧版端点签名/所有权文件(system-proxy-endpoint-signature.json、
|
||
// system-proxy-ownership.json)按 issue #71 就地遗留:不读取、不迁移、
|
||
// 不删除。
|
||
}
|
||
|
||
/// ACL 活动链接的防逃逸校验(ADR-0011):解析 symlink 后必须仍是运行目录
|
||
/// 内的普通文件。目录与目标都做 symlink 解析,避免 `/var` vs `/private/var`
|
||
/// 造成假阴性。GUI 写入侧与 Agent 加载侧共用此判定。
|
||
enum ACLActiveLinkPolicy {
|
||
static func resolvesToRegularFile(inside directory: URL, linkURL: URL) -> Bool {
|
||
let fileManager = FileManager.default
|
||
let linkPath = linkURL.standardizedFileURL.path
|
||
guard fileManager.fileExists(atPath: linkPath) else { return false }
|
||
let resolved = URL(fileURLWithPath: linkPath).resolvingSymlinksInPath().standardizedFileURL
|
||
let resolvedDirectory = URL(fileURLWithPath: directory.path).resolvingSymlinksInPath()
|
||
.standardizedFileURL.path
|
||
guard resolved.path == resolvedDirectory || resolved.path.hasPrefix(resolvedDirectory + "/")
|
||
else { return false }
|
||
var isDirectory: ObjCBool = false
|
||
guard fileManager.fileExists(atPath: resolved.path, isDirectory: &isDirectory) else {
|
||
return false
|
||
}
|
||
return !isDirectory.boolValue
|
||
}
|
||
}
|