mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-14 10:13:54 +00:00
与导入分离的「切换到 2.0」确认流程(spec #21 D12): - Domain/LegacyHandoff:硬编码白名单三个 Legacy label(.local/.http/.kcptun), bootout 按 service target(覆盖 plist 缺失错位形态),print 退出码验证 (0/113),`launchctl kill SIGTERM` 有文档后备,绝不泛匹配、不 pkill; 残留 plist(含 KeepAlive 形态)读内容判活跃性后 disable,阻止下次登录复活; 系统代理按特征值判定归属(Legacy 自身 off 模式的判定镜像:本地 PAC URL http://localhost:<port>/proxy.pac、SOCKS/HTTP 地址端口),仅清理归属为 Legacy 的服务,未知所有者点名保留;端口释放门禁用无复用选项的 bind 探测 (Legacy ss-local 带 --reuse-port),未释放不启动 2.0;Legacy 数据零删除。 - App 层:SystemConfigurationLegacyProxyCleaner(授权在最前、失败零影响、 事务回滚);WorkspaceLegacyAppController(共享 bundle id 需排除自身进程); LegacyHandoffSheet 确认/进度/报告呈现,含旧登录项人工指引与残留提示; 主窗口工具栏入口 + 导入完成后「切换到 2.0」入口;交接成功经正常启停入口 启动 2.0(健康门禁走 #29 路径)。 - bootout 实弹验证(Further Notes #5)闭环:docs/research/ legacy-handoff-bootout-live-fire.md——bootout 未加载 label 退出码为 3 (非 print 的 113),KeepAlive 残留 kill 后复活、bootout 后不再加载, 结论已回写产品代码与测试。
204 lines
7.8 KiB
Swift
204 lines
7.8 KiB
Swift
import Foundation
|
|
import Security
|
|
import SystemConfiguration
|
|
|
|
/// Legacy 写入的系统代理清理(issue #37):读取当前网络位置每个服务的
|
|
/// Proxies 字典并按 Legacy 特征值判定归属,只改写判定为 Legacy 的服务
|
|
/// (禁用全部代理面、移除端点值);空配置跳过;未知所有者原样保留并点名
|
|
/// 返回,绝不覆盖。事务纪律与 SystemConfigurationProxyController 一致:
|
|
/// 写入失败回滚到本次操作前状态。实现只抛 LegacyHandoffError.proxyCleanFailed。
|
|
final class SystemConfigurationLegacyProxyCleaner: LegacyProxyCleaning {
|
|
private struct ServiceSnapshot {
|
|
let id: String
|
|
let proxyProtocol: SCNetworkProtocol
|
|
let configuration: Data?
|
|
}
|
|
|
|
func cleanLegacyOwnedProxy(ports: LegacyListenPorts) throws -> LegacyProxyCleanOutcome {
|
|
do {
|
|
return try withPreferences { preferences in
|
|
let (legacyServices, unknownOwnerServiceIDs) =
|
|
try classifyServices(in: preferences, ports: ports)
|
|
guard !legacyServices.isEmpty else {
|
|
return LegacyProxyCleanOutcome(
|
|
cleanedServiceIDs: [], unknownOwnerServiceIDs: unknownOwnerServiceIDs)
|
|
}
|
|
let cleanedServiceIDs = try clean(legacyServices, in: preferences)
|
|
return LegacyProxyCleanOutcome(
|
|
cleanedServiceIDs: cleanedServiceIDs,
|
|
unknownOwnerServiceIDs: unknownOwnerServiceIDs)
|
|
}
|
|
} catch let error as LegacyHandoffError {
|
|
throw error
|
|
} catch {
|
|
throw LegacyHandoffError.proxyCleanFailed(detail: describe(error))
|
|
}
|
|
}
|
|
|
|
// MARK: - SystemConfiguration 管道(与 SystemConfigurationProxyController 同纪律)
|
|
|
|
/// 归属分类(只读):Legacy 服务捕获原字典供清理投影;未知所有者点名返回。
|
|
private func classifyServices(
|
|
in preferences: SCPreferences, ports: LegacyListenPorts
|
|
) throws -> (
|
|
legacy: [(ServiceSnapshot, [String: Any])], unknownOwnerIDs: [String]
|
|
) {
|
|
let services = try serviceSnapshots(in: preferences)
|
|
guard !services.isEmpty else {
|
|
throw LegacyHandoffError.proxyCleanFailed(
|
|
detail: SystemProxyError.noProxyServices.presentedReason)
|
|
}
|
|
var legacyServices: [(ServiceSnapshot, [String: Any])] = []
|
|
var unknownOwnerIDs: [String] = []
|
|
for service in services {
|
|
let dictionary = try readableDictionary(
|
|
from: service.configuration, serviceID: service.id)
|
|
switch LegacySystemProxySignature.classify(dictionary, ports: ports) {
|
|
case .none:
|
|
continue
|
|
case .legacy:
|
|
legacyServices.append((service, dictionary))
|
|
case .other:
|
|
unknownOwnerIDs.append(service.id)
|
|
}
|
|
}
|
|
return (legacyServices, unknownOwnerIDs)
|
|
}
|
|
|
|
/// 写入并提交;失败回滚到本次操作前状态。
|
|
private func clean(
|
|
_ legacyServices: [(ServiceSnapshot, [String: Any])], in preferences: SCPreferences
|
|
) throws -> [String] {
|
|
try SCPreferencesLockOrThrow(preferences)
|
|
defer { SCPreferencesUnlock(preferences) }
|
|
do {
|
|
for (service, dictionary) in legacyServices {
|
|
let cleaned = LegacySystemProxySignature.cleaned(dictionary)
|
|
guard SCNetworkProtocolSetConfiguration(service.proxyProtocol, cleaned as CFDictionary)
|
|
else {
|
|
throw LegacyHandoffError.proxyCleanFailed(
|
|
detail: SystemProxyError.cannotWriteService(service.id).presentedReason)
|
|
}
|
|
}
|
|
try commitAndApply(preferences)
|
|
} catch {
|
|
let rollbackSucceeded = rollback(legacyServices, in: preferences)
|
|
var detail = describe(error)
|
|
if !rollbackSucceeded {
|
|
detail += ";系统代理变更回滚失败:\(systemConfigurationError())"
|
|
}
|
|
throw LegacyHandoffError.proxyCleanFailed(detail: detail)
|
|
}
|
|
return legacyServices.map { $0.0.id }
|
|
}
|
|
|
|
private func withPreferences(
|
|
_ body: (SCPreferences) throws -> LegacyProxyCleanOutcome
|
|
) throws -> LegacyProxyCleanOutcome {
|
|
var authorization: AuthorizationRef?
|
|
let flags: AuthorizationFlags = [.interactionAllowed, .extendRights, .preAuthorize]
|
|
let authorizationStatus = AuthorizationCreate(nil, nil, flags, &authorization)
|
|
guard authorizationStatus == errAuthorizationSuccess, let authorization else {
|
|
throw LegacyHandoffError.proxyCleanFailed(
|
|
detail: SystemProxyError.authorizationFailed(authorizationStatus).presentedReason)
|
|
}
|
|
defer { AuthorizationFree(authorization, []) }
|
|
|
|
guard
|
|
let preferences = SCPreferencesCreateWithAuthorization(
|
|
nil, "ShadowsocksX-NG" as CFString, nil, authorization)
|
|
else {
|
|
throw LegacyHandoffError.proxyCleanFailed(
|
|
detail: SystemProxyError.preferencesUnavailable.presentedReason)
|
|
}
|
|
return try body(preferences)
|
|
}
|
|
|
|
private func serviceSnapshots(in preferences: SCPreferences) throws -> [ServiceSnapshot] {
|
|
guard let set = SCNetworkSetCopyCurrent(preferences) else {
|
|
throw LegacyHandoffError.proxyCleanFailed(
|
|
detail: SystemProxyError.noCurrentNetworkSet.presentedReason)
|
|
}
|
|
guard let services = SCNetworkSetCopyServices(set) as? [SCNetworkService] else {
|
|
throw LegacyHandoffError.proxyCleanFailed(
|
|
detail: SystemProxyError.noProxyServices.presentedReason)
|
|
}
|
|
return try services.compactMap { service in
|
|
guard
|
|
let serviceID = SCNetworkServiceGetServiceID(service) as String?,
|
|
let proxyProtocol = SCNetworkServiceCopyProtocol(service, kSCNetworkProtocolTypeProxies)
|
|
else { return nil }
|
|
return ServiceSnapshot(
|
|
id: serviceID,
|
|
proxyProtocol: proxyProtocol,
|
|
configuration: try propertyListData(
|
|
from: SCNetworkProtocolGetConfiguration(proxyProtocol), serviceID: serviceID))
|
|
}
|
|
}
|
|
|
|
private func rollback(
|
|
_ services: [(ServiceSnapshot, [String: Any])], in preferences: SCPreferences
|
|
) -> Bool {
|
|
for (service, original) in services {
|
|
guard SCNetworkProtocolSetConfiguration(service.proxyProtocol, original as CFDictionary)
|
|
else {
|
|
return false
|
|
}
|
|
}
|
|
return SCPreferencesCommitChanges(preferences) && SCPreferencesApplyChanges(preferences)
|
|
}
|
|
|
|
private func readableDictionary(
|
|
from data: Data?, serviceID: String
|
|
) throws -> [String: Any] {
|
|
guard let data else { return [:] }
|
|
guard
|
|
let propertyList = try? PropertyListSerialization.propertyList(
|
|
from: data, options: [], format: nil),
|
|
let dictionary = propertyList as? [String: Any]
|
|
else {
|
|
throw LegacyHandoffError.proxyCleanFailed(
|
|
detail: SystemProxyError.invalidStoredConfiguration(serviceID).presentedReason)
|
|
}
|
|
return dictionary
|
|
}
|
|
|
|
private func propertyListData(
|
|
from value: CFPropertyList?, serviceID: String
|
|
) throws -> Data? {
|
|
guard let value else { return nil }
|
|
guard let dictionary = value as? [String: Any] else {
|
|
throw LegacyHandoffError.proxyCleanFailed(
|
|
detail: SystemProxyError.unreadableService(serviceID).presentedReason)
|
|
}
|
|
return try PropertyListSerialization.data(
|
|
fromPropertyList: dictionary, format: .binary, options: 0)
|
|
}
|
|
|
|
private func commitAndApply(_ preferences: SCPreferences) throws {
|
|
guard SCPreferencesCommitChanges(preferences) else {
|
|
throw LegacyHandoffError.proxyCleanFailed(
|
|
detail: "提交失败:\(systemConfigurationError())")
|
|
}
|
|
guard SCPreferencesApplyChanges(preferences) else {
|
|
throw LegacyHandoffError.proxyCleanFailed(
|
|
detail: "应用失败:\(systemConfigurationError())")
|
|
}
|
|
}
|
|
|
|
private func describe(_ error: Error) -> String {
|
|
(error as? SystemProxyError)?.presentedReason ?? String(describing: error)
|
|
}
|
|
|
|
private func systemConfigurationError() -> String {
|
|
String(cString: SCErrorString(SCError()))
|
|
}
|
|
}
|
|
|
|
private func SCPreferencesLockOrThrow(_ preferences: SCPreferences) throws {
|
|
guard SCPreferencesLock(preferences, true) else {
|
|
throw LegacyHandoffError.proxyCleanFailed(
|
|
detail: SystemProxyError.preferencesBusy.presentedReason)
|
|
}
|
|
}
|