























@@ -10,6 +10,7 @@ final class MacNodeModeCoordinator {
1010private var task: Task<Void, Never>?
1111private let runtime = MacNodeRuntime()
1212private let session = GatewayNodeSession()
13+private var autoRepairedTLSFingerprintsByStoreKey: [String: String] = [:]
13141415func start() {
1516guard self.task == nil else { return }
@@ -58,8 +59,10 @@ final class MacNodeModeCoordinator {
5859try? await Task.sleep(nanoseconds: 200_000_000)
5960}
606162+var attemptedURL: URL?
6163do {
6264let config = try await GatewayEndpointStore.shared.requireConfig()
65+ attemptedURL = config.url
6366let caps = self.currentCaps()
6467let commands = self.currentCommands(caps: caps)
6568let permissions = await self.currentPermissions()
@@ -109,6 +112,10 @@ final class MacNodeModeCoordinator {
109112 retryDelay = 1_000_000_000
110113try? await Task.sleep(nanoseconds: 1_000_000_000)
111114} catch {
115+if await self.autoRepairStaleTLSPinIfNeeded(error: error, url: attemptedURL) {
116+ retryDelay = 1_000_000_000
117+continue
118+}
112119self.logger.error("mac node gateway connect failed: \(error.localizedDescription, privacy: .public)")
113120try? await Task.sleep(nanoseconds: min(retryDelay, 10_000_000_000))
114121 retryDelay = min(retryDelay * 2, 10_000_000_000)
@@ -188,11 +195,49 @@ final class MacNodeModeCoordinator {
188195Self.resolvedCommands(caps: caps)
189196}
190197198+nonisolated static func tlsPinStoreKey(for url: URL) -> String {
199+let host = url.host?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? "gateway"
200+let port = url.port ?? 443
201+return "\(host):\(port)"
202+}
203+204+nonisolated static func shouldAutoRepairStaleTLSPin(url: URL, failure: GatewayTLSValidationFailure) -> Bool {
205+guard failure.kind == .pinMismatch else { return false }
206+guard url.scheme?.lowercased() == "wss" else { return false }
207+guard failure.storeKey == nil || failure.storeKey == self.tlsPinStoreKey(for: url) else { return false }
208+guard let host = url.host?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(), !host.isEmpty
209+else { return false }
210+211+if LoopbackHost.isLoopback(host) {
212+return failure.systemTrustOk
213+}
214+215+ // Tailscale Serve uses publicly trusted, rotating certificates for *.ts.net names.
216+ // A stale legacy leaf pin should not leave the companion app half-connected forever.
217+if host == "ts.net" || host.hasSuffix(".ts.net") {
218+return failure.systemTrustOk
219+}
220+221+return false
222+}
223+224+private func autoRepairStaleTLSPinIfNeeded(error: Error, url: URL?) async -> Bool {
225+guard let tlsError = error as? GatewayTLSValidationError, let url else { return false }
226+guard Self.shouldAutoRepairStaleTLSPin(url: url, failure: tlsError.failure) else { return false }
227+let storeKey = tlsError.failure.storeKey ?? Self.tlsPinStoreKey(for: url)
228+guard let observedFingerprint = tlsError.failure.observedFingerprint else { return false }
229+guard self.autoRepairedTLSFingerprintsByStoreKey[storeKey] != observedFingerprint else { return false }
230+231+guard GatewayTLSStore.replaceFingerprint(observedFingerprint, stableID: storeKey) else { return false }
232+self.autoRepairedTLSFingerprintsByStoreKey[storeKey] = observedFingerprint
233+self.logger.info("replaced stale gateway TLS pin storeKey=\(storeKey, privacy: .public)")
234+await self.session.disconnect()
235+return true
236+}
237+191238private func buildSessionBox(url: URL) -> WebSocketSessionBox? {
192239guard url.scheme?.lowercased() == "wss" else { return nil }
193-let host = url.host ?? "gateway"
194-let port = url.port ?? 443
195-let stableID = "\(host):\(port)"
240+let stableID = Self.tlsPinStoreKey(for: url)
196241let stored = GatewayTLSStore.loadFingerprint(stableID: stableID)
197242let params = GatewayTLSParams(
198243 required: true,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。