From 8ca2da494dfbb80d59428b725b09e50604898c00 Mon Sep 17 00:00:00 2001 From: Qiu Yuzhou Date: Sat, 17 Oct 2026 15:30:47 +0800 Subject: [PATCH] refactor(runtime): drop ProxyModeError guards and silent desired-proxy skip Make systemProxyConfiguration total so a runtime document can no longer swallow derivation via try? and silently skip desired system proxy convergence. Delete the unreachable ProxyModeError / .mode failure fact and its presentation; port validity stays at the settings store and the document contract. --- .../ProxyRuntimeController+Facts.swift | 4 +-- .../ProxyRuntime/ProxyRuntimeFacts.swift | 1 - .../ProxyRuntime/SystemProxyObserver.swift | 3 -- .../ErrorPresentation+Runtime.swift | 1 - .../App/Presentation/ErrorPresentation.swift | 9 ----- .../App/Resources/Localizable.xcstrings | 34 ------------------- ShadowsocksX-NG2/Domain/Rules/ProxyMode.swift | 15 ++------ .../Runtime/SslocalRuntimeDocument.swift | 3 +- .../ProxyModeAvailabilityTests.swift | 12 +++---- .../SystemProxyObserverTests.swift | 2 +- .../Tests/SystemProxy/SystemProxyTests.swift | 14 ++++---- .../Workspace/AppPresentationTests.swift | 1 - .../0012-system-proxy-covers-http-https.md | 2 +- 13 files changed, 22 insertions(+), 79 deletions(-) diff --git a/ShadowsocksX-NG2/App/Application/ProxyRuntime/ProxyRuntimeController+Facts.swift b/ShadowsocksX-NG2/App/Application/ProxyRuntime/ProxyRuntimeController+Facts.swift index 807a70a6..60d3733a 100644 --- a/ShadowsocksX-NG2/App/Application/ProxyRuntime/ProxyRuntimeController+Facts.swift +++ b/ShadowsocksX-NG2/App/Application/ProxyRuntime/ProxyRuntimeController+Facts.swift @@ -20,10 +20,10 @@ extension ProxyRuntimeController { } /// Desired system proxy configuration:当前保存设置要求的代理值(ADR-0022)。 - /// 观察机经 init 闭包读取同一事实源。 + /// 观察机经 init 闭包读取同一事实源。`nil` 只表示尚无运行时文档。 var desiredSystemProxyConfiguration: SystemProxyConfiguration? { guard let document = lastDocument else { return nil } - return try? proxyMode.systemProxyConfiguration( + return proxyMode.systemProxyConfiguration( for: document, exceptions: settings.proxyExceptionList) } diff --git a/ShadowsocksX-NG2/App/Application/ProxyRuntime/ProxyRuntimeFacts.swift b/ShadowsocksX-NG2/App/Application/ProxyRuntime/ProxyRuntimeFacts.swift index 5e5a9012..243907bc 100644 --- a/ShadowsocksX-NG2/App/Application/ProxyRuntime/ProxyRuntimeFacts.swift +++ b/ShadowsocksX-NG2/App/Application/ProxyRuntime/ProxyRuntimeFacts.swift @@ -134,7 +134,6 @@ extension SystemProxyOperationFailure { enum SystemProxyFailureFacts: Equatable, Sendable { case operation(SystemProxyOperationFailure) - case mode(ProxyModeError) case unknown } diff --git a/ShadowsocksX-NG2/App/Application/ProxyRuntime/SystemProxyObserver.swift b/ShadowsocksX-NG2/App/Application/ProxyRuntime/SystemProxyObserver.swift index 795d2910..f5e0533f 100644 --- a/ShadowsocksX-NG2/App/Application/ProxyRuntime/SystemProxyObserver.swift +++ b/ShadowsocksX-NG2/App/Application/ProxyRuntime/SystemProxyObserver.swift @@ -596,9 +596,6 @@ extension SystemProxyObserver { if let error = error as? SystemProxyError { return .operation(SystemProxyOperationFailure(error)) } - if let error = error as? ProxyModeError { - return .mode(error) - } return .unknown } } diff --git a/ShadowsocksX-NG2/App/Presentation/ErrorPresentation+Runtime.swift b/ShadowsocksX-NG2/App/Presentation/ErrorPresentation+Runtime.swift index cfc79584..7ef3955e 100644 --- a/ShadowsocksX-NG2/App/Presentation/ErrorPresentation+Runtime.swift +++ b/ShadowsocksX-NG2/App/Presentation/ErrorPresentation+Runtime.swift @@ -62,7 +62,6 @@ extension AppPresentation { private static func systemProxyFailure(_ facts: SystemProxyFailureFacts) -> String { switch facts { case .operation(let failure): return systemProxy(failure) - case .mode(let error): return proxyMode(error) case .unknown: return String(localized: "系统代理未能应用") } } diff --git a/ShadowsocksX-NG2/App/Presentation/ErrorPresentation.swift b/ShadowsocksX-NG2/App/Presentation/ErrorPresentation.swift index e9d5c95f..3dc4edfc 100644 --- a/ShadowsocksX-NG2/App/Presentation/ErrorPresentation.swift +++ b/ShadowsocksX-NG2/App/Presentation/ErrorPresentation.swift @@ -51,8 +51,6 @@ extension AppPresentation { return reason(error) case let error as CommitError: return commit(error) - case let error as ProxyModeError: - return proxyMode(error) case let error as PortSettingError: return port(error) case let error as ProxySettingsStoreError: @@ -275,13 +273,6 @@ extension AppPresentation { } } - static func proxyMode(_ error: ProxyModeError) -> String { - switch error { - case .invalidSOCKSPort: return String(localized: "SOCKS 端口无效,必须是 1025–65535 之间的整数") - case .invalidHTTPPort: return String(localized: "HTTP 端口无效,必须是 1025–65535 之间的整数") - } - } - private static func port(_ error: PortSettingError) -> String { switch error { case .portOutOfRange(let endpoint, let portNumber): diff --git a/ShadowsocksX-NG2/App/Resources/Localizable.xcstrings b/ShadowsocksX-NG2/App/Resources/Localizable.xcstrings index 07e41341..6b4acd14 100644 --- a/ShadowsocksX-NG2/App/Resources/Localizable.xcstrings +++ b/ShadowsocksX-NG2/App/Resources/Localizable.xcstrings @@ -2932,23 +2932,6 @@ } } }, - "HTTP 端口无效,必须是 1025–65535 之间的整数": { - "extractionState": "manual", - "localizations": { - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "HTTP 端口无效,必须是 1025–65535 之间的整数" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Invalid HTTP port. Enter an integer between 1025 and 65535." - } - } - } - }, "Legacy 导入": { "extractionState": "manual", "localizations": { @@ -3051,23 +3034,6 @@ } } }, - "SOCKS 端口无效,必须是 1025–65535 之间的整数": { - "extractionState": "manual", - "localizations": { - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "SOCKS 端口无效,必须是 1025–65535 之间的整数" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Invalid SOCKS port. Enter an integer between 1025 and 65535." - } - } - } - }, "URI 链接列表文本文件写入失败": { "extractionState": "manual", "localizations": { diff --git a/ShadowsocksX-NG2/Domain/Rules/ProxyMode.swift b/ShadowsocksX-NG2/Domain/Rules/ProxyMode.swift index 6e290754..1b4fc2fa 100644 --- a/ShadowsocksX-NG2/Domain/Rules/ProxyMode.swift +++ b/ShadowsocksX-NG2/Domain/Rules/ProxyMode.swift @@ -77,16 +77,12 @@ enum ProxyMode: Codable, Equatable, Hashable, Sendable { /// SOCKS 之外,系统 HTTP/HTTPS 代理指向恒开启的 HTTP 入站。 /// 投影即 closed typed 配置:HTTPS 显式赋值、PAC/自动发现显式 /// 关闭、简单主机名排除显式开启、例外列表全量给出。 + /// 端口合法性由 settings store 与运行时文档派生保证,投影对任意 + /// 文档都是全函数,保证期望系统代理配置不被静默跳过(ADR-0022)。 func systemProxyConfiguration( for document: SslocalRuntimeDocument, exceptions: [String] = [] - ) throws -> SystemProxyConfiguration { - guard ProxyPortRange.valid.contains(document.socksPort) else { - throw ProxyModeError.invalidSOCKSPort(document.socksPort) - } - guard ProxyPortRange.valid.contains(document.httpPort) else { - throw ProxyModeError.invalidHTTPPort(document.httpPort) - } + ) -> SystemProxyConfiguration { let loopback = "127.0.0.1" return SystemProxyConfiguration( socks: .init(host: loopback, port: document.socksPort), @@ -96,8 +92,3 @@ enum ProxyMode: Codable, Equatable, Hashable, Sendable { exceptions: FixedLocalProxyRanges.systemProxyExceptions(including: exceptions)) } } - -enum ProxyModeError: Error, Equatable, Sendable { - case invalidSOCKSPort(Int) - case invalidHTTPPort(Int) -} diff --git a/ShadowsocksX-NG2/Domain/Runtime/SslocalRuntimeDocument.swift b/ShadowsocksX-NG2/Domain/Runtime/SslocalRuntimeDocument.swift index 44eb3b5b..4c9d2fc5 100644 --- a/ShadowsocksX-NG2/Domain/Runtime/SslocalRuntimeDocument.swift +++ b/ShadowsocksX-NG2/Domain/Runtime/SslocalRuntimeDocument.swift @@ -228,7 +228,8 @@ extension SslocalRuntimeDocument { var socksPort: Int { socksLocal?.localPort ?? 0 } var socksMode: String { socksLocal?.mode ?? "" } - /// HTTP 入站端口;缺 HTTP 入站时为 0(无效端口,系统代理投影层拒绝)。 + /// HTTP 入站端口;缺 HTTP 入站时为 0。完整入站组由 `isWellFormed` 在 + /// 跨进程契约缝拒绝,端口合法性由 settings store 保证。 var httpPort: Int { locals.first { $0.inboundProtocol == "http" }?.localPort ?? 0 } diff --git a/ShadowsocksX-NG2/Tests/ProxyControl/ProxyModeAvailabilityTests.swift b/ShadowsocksX-NG2/Tests/ProxyControl/ProxyModeAvailabilityTests.swift index 84931426..7d1754b4 100644 --- a/ShadowsocksX-NG2/Tests/ProxyControl/ProxyModeAvailabilityTests.swift +++ b/ShadowsocksX-NG2/Tests/ProxyControl/ProxyModeAvailabilityTests.swift @@ -12,10 +12,10 @@ final class ProxyModeAvailabilityTests: XCTestCase { [.rule, .global, .direct]) } - func testDirectModeUsesLocalSOCKSAndIncludesFixedBypasses() throws { + func testDirectModeUsesLocalSOCKSAndIncludesFixedBypasses() { let document = SslocalRuntimeDocument(servers: [], listen: SslocalListenSettings()) - let configuration = try ProxyMode.direct.systemProxyConfiguration(for: document) + let configuration = ProxyMode.direct.systemProxyConfiguration(for: document) XCTAssertEqual( configuration.socks, @@ -28,11 +28,11 @@ final class ProxyModeAvailabilityTests: XCTestCase { FixedLocalProxyRanges.systemProxyExceptions) } - func testIPv6LANModeKeepsLocalSystemProxyEndpoints() throws { + func testIPv6LANModeKeepsLocalSystemProxyEndpoints() { let listen = SslocalListenSettings(listenerMode: .allIPv6Interfaces) let document = SslocalRuntimeDocument(servers: [], listen: listen) - let configuration = try ProxyMode.direct.systemProxyConfiguration(for: document) + let configuration = ProxyMode.direct.systemProxyConfiguration(for: document) XCTAssertEqual(configuration.socks, .init(host: "127.0.0.1", port: listen.socksPort)) XCTAssertEqual(configuration.http, .init(host: "127.0.0.1", port: listen.httpPort)) @@ -40,10 +40,10 @@ final class ProxyModeAvailabilityTests: XCTestCase { /// 全局模式:系统 SOCKS/HTTP 投影 + 固定本地例外,与直连共用 /// 同一安全范围;公网路由由 ACL 决定,不由系统例外决定。 - func testGlobalModeUsesLocalSOCKSAndIncludesFixedBypasses() throws { + func testGlobalModeUsesLocalSOCKSAndIncludesFixedBypasses() { let document = SslocalRuntimeDocument(servers: [], listen: SslocalListenSettings()) - let configuration = try ProxyMode.global.systemProxyConfiguration(for: document) + let configuration = ProxyMode.global.systemProxyConfiguration(for: document) XCTAssertEqual( configuration.socks, diff --git a/ShadowsocksX-NG2/Tests/SystemProxy/SystemProxyObserverTests.swift b/ShadowsocksX-NG2/Tests/SystemProxy/SystemProxyObserverTests.swift index 640f88d6..0b7272cb 100644 --- a/ShadowsocksX-NG2/Tests/SystemProxy/SystemProxyObserverTests.swift +++ b/ShadowsocksX-NG2/Tests/SystemProxy/SystemProxyObserverTests.swift @@ -151,7 +151,7 @@ final class SystemProxyObserverTests: XCTestCase { isIntentEnabled: isIntentEnabled, isExitAvailable: { true }, desiredConfiguration: { - try? ProxyMode.direct.systemProxyConfiguration( + ProxyMode.direct.systemProxyConfiguration( for: SslocalRuntimeDocument(servers: [], listen: SslocalListenSettings()), exceptions: []) }, diff --git a/ShadowsocksX-NG2/Tests/SystemProxy/SystemProxyTests.swift b/ShadowsocksX-NG2/Tests/SystemProxy/SystemProxyTests.swift index b9d046dc..8ca82a3f 100644 --- a/ShadowsocksX-NG2/Tests/SystemProxy/SystemProxyTests.swift +++ b/ShadowsocksX-NG2/Tests/SystemProxy/SystemProxyTests.swift @@ -7,7 +7,7 @@ import XCTest /// Closed typed 配置覆盖 HTTPS/PAC/自动发现/简单主机名与例外列表, /// 无条件清理计划与 helper XPC 契约(含序列化 last-writer)都在此验证。 final class SystemProxyTests: XCTestCase { - func testSupportedModesProjectTheSameLocalEndpoints() throws { + func testSupportedModesProjectTheSameLocalEndpoints() { let document = ProxyRuntimeFixture.makeDocument( listenerMode: .allIPv4Interfaces, localPort: 2086) @@ -17,19 +17,19 @@ final class SystemProxyTests: XCTestCase { https: .init(host: "127.0.0.1", port: SslocalListenSettings.defaultHTTPPort), exceptions: FixedLocalProxyRanges.systemProxyExceptions) XCTAssertEqual( - try ProxyMode.rule.systemProxyConfiguration(for: document), + ProxyMode.rule.systemProxyConfiguration(for: document), expected) XCTAssertEqual( - try ProxyMode.global.systemProxyConfiguration(for: document), + ProxyMode.global.systemProxyConfiguration(for: document), expected, "全局模式的系统例外使用固定本地范围,与 ACL 安全策略一致") } - func testIPv6LANModeKeepsSystemProxyOnLocalIPv4Loopback() throws { + func testIPv6LANModeKeepsSystemProxyOnLocalIPv4Loopback() { let document = SslocalRuntimeDocument( servers: [], listen: SslocalListenSettings(listenerMode: .allIPv6Interfaces)) - let configuration = try ProxyMode.rule.systemProxyConfiguration(for: document) + let configuration = ProxyMode.rule.systemProxyConfiguration(for: document) XCTAssertEqual(configuration.socks.host, "127.0.0.1") XCTAssertEqual(configuration.http.host, "127.0.0.1") @@ -51,10 +51,10 @@ final class SystemProxyTests: XCTestCase { XCTAssertNil(SslocalRuntimeDocument.decodeValidated(data), "每组必须同时提供 SOCKS 与 HTTP") } - func testModeProjectionCarriesClosedTypedPolicyDefaults() throws { + func testModeProjectionCarriesClosedTypedPolicyDefaults() { let document = ProxyRuntimeFixture.makeDocument(localPort: 2086) - let configuration = try ProxyMode.rule.systemProxyConfiguration( + let configuration = ProxyMode.rule.systemProxyConfiguration( for: document, exceptions: ["example.com"]) XCTAssertTrue(configuration.pacEnabled == false, "PAC 显式关闭") diff --git a/ShadowsocksX-NG2/Tests/Workspace/AppPresentationTests.swift b/ShadowsocksX-NG2/Tests/Workspace/AppPresentationTests.swift index 945d95c4..c6c8da99 100644 --- a/ShadowsocksX-NG2/Tests/Workspace/AppPresentationTests.swift +++ b/ShadowsocksX-NG2/Tests/Workspace/AppPresentationTests.swift @@ -61,7 +61,6 @@ final class AppPresentationTests: XCTestCase { .pending, .applied, .failed(.operation(.applyFailed)), - .failed(.mode(.invalidSOCKSPort(0))), .failed(.operation(.helperUnavailable)), .failed(.unknown), ] diff --git a/docs/adr/0012-system-proxy-covers-http-https.md b/docs/adr/0012-system-proxy-covers-http-https.md index 23824c1c..38ef03a4 100644 --- a/docs/adr/0012-system-proxy-covers-http-https.md +++ b/docs/adr/0012-system-proxy-covers-http-https.md @@ -4,4 +4,4 @@ Enabling the system proxy switch writes the macOS HTTP and HTTPS proxy entries in addition to the SOCKS entry (ADR-0007 had disabled the HTTP/HTTPS families and projected a SOCKS target only). All three families point at 2.0's own local inbounds: SOCKS at the SOCKS5 listener, and HTTP and HTTPS at the always-on local HTTP inbound — shadowsocks-rust serves HTTP CONNECT directly, so no Privoxy-style bridge returns. HTTPS and HTTP share one endpoint because both are served by the same CONNECT-capable inbound. Exceptions and PAC/auto-discovery suppression remain; the ownership snapshot/restore protocol retained here was superseded by ADR-0019. -The launch health gate already probes every configured inbound, so the HTTP endpoint must be reachable before any system proxy write happens. A runtime document without a usable HTTP inbound port is rejected by the mode projection (`invalidHTTPPort`) instead of pointing the system at an invalid port. At the time of this decision NG2 had not shipped, so no migration was planned; ADR-0019 later replaced pre-app snapshot restoration with endpoint-matched cleanup. +The launch health gate already probes every configured inbound, so the HTTP endpoint must be reachable before any system proxy write happens. A runtime document without a usable HTTP inbound port is rejected at the runtime-document contract (`isWellFormed`) instead of pointing the system at an invalid port; the mode projection itself is total over any document. At the time of this decision NG2 had not shipped, so no migration was planned; ADR-0019 later replaced pre-app snapshot restoration with endpoint-matched cleanup.