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.
This commit is contained in:
Qiu Yuzhou
2026-10-17 15:30:47 +08:00
parent 346af7fdae
commit 8ca2da494d
13 changed files with 22 additions and 79 deletions
@@ -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)
}
@@ -134,7 +134,6 @@ extension SystemProxyOperationFailure {
enum SystemProxyFailureFacts: Equatable, Sendable {
case operation(SystemProxyOperationFailure)
case mode(ProxyModeError)
case unknown
}
@@ -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
}
}
@@ -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: "系统代理未能应用")
}
}
@@ -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):
@@ -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": {
+3 -12
View File
@@ -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)
}
@@ -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
}
@@ -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,
@@ -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: [])
},
@@ -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 显式关闭")
@@ -61,7 +61,6 @@ final class AppPresentationTests: XCTestCase {
.pending,
.applied,
.failed(.operation(.applyFailed)),
.failed(.mode(.invalidSOCKSPort(0))),
.failed(.operation(.helperUnavailable)),
.failed(.unknown),
]
@@ -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.