Skip to content

Commit 9cbf182

Browse files
clawsweeper[bot]zhangguiping-xydtTakhoffman
authored
fix #90668: [Bug]: macOS node mode can silently self-reconnect in a healthy direct gateway session (#90815)
Summary: - Adds a macOS node-mode TLS session cache keyed by gateway URL and TLS pin parameters, with Swift tests for reuse and rebuild behavior. - PR surface: Other +78. Total +78 across 2 files. - Reproducibility: yes. The source path is clear: current main supplies a fresh TLS session identity into `Gat ... inked macOS WSS proof demonstrates repeated connected callbacks before the cache and one callback after it. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(macos): make TLS session cache lint-safe - PR branch already contained follow-up commit before automerge: fix #90668: [Bug]: macOS node mode can silently self-reconnect in a h… Validation: - ClawSweeper review passed for head 1496eac. - Required merge gates passed before the squash merge. Prepared head SHA: 1496eac Review: #90815 (comment) Co-authored-by: 张贵萍0668001030 <zhang.guiping@xydigit.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
1 parent 50aaf1f commit 9cbf182

2 files changed

Lines changed: 82 additions & 4 deletions

File tree

apps/macos/Sources/OpenClaw/NodeMode/MacNodeModeCoordinator.swift

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,43 @@ import Foundation
22
import OpenClawKit
33
import OSLog
44

5+
struct MacNodeGatewayTLSSessionCache {
6+
private struct Key: Equatable {
7+
let url: URL
8+
let required: Bool
9+
let expectedFingerprint: String?
10+
let allowTOFU: Bool
11+
let storeKey: String?
12+
13+
init(url: URL, params: GatewayTLSParams) {
14+
self.url = url
15+
self.required = params.required
16+
self.expectedFingerprint = params.expectedFingerprint
17+
self.allowTOFU = params.allowTOFU
18+
self.storeKey = params.storeKey
19+
}
20+
}
21+
22+
private var cachedKey: Key?
23+
private var cachedBox: WebSocketSessionBox?
24+
25+
mutating func sessionBox(url: URL, params: GatewayTLSParams) -> WebSocketSessionBox {
26+
let key = Key(url: url, params: params)
27+
if let cachedKey = self.cachedKey, cachedKey == key, let cachedBox = self.cachedBox {
28+
return cachedBox
29+
}
30+
let box = WebSocketSessionBox(session: GatewayTLSPinningSession(params: params))
31+
self.cachedKey = key
32+
self.cachedBox = box
33+
return box
34+
}
35+
36+
mutating func invalidate() {
37+
self.cachedKey = nil
38+
self.cachedBox = nil
39+
}
40+
}
41+
542
@MainActor
643
final class MacNodeModeCoordinator {
744
static let shared = MacNodeModeCoordinator()
@@ -11,6 +48,7 @@ final class MacNodeModeCoordinator {
1148
private let runtime: MacNodeRuntime
1249
private let session: GatewayNodeSession
1350
private var autoRepairedTLSFingerprintsByStoreKey: [String: String] = [:]
51+
private var tlsSessionCache = MacNodeGatewayTLSSessionCache()
1452

1553
private init() {
1654
let session = GatewayNodeSession()
@@ -268,16 +306,21 @@ final class MacNodeModeCoordinator {
268306
}
269307

270308
private func buildSessionBox(url: URL, connectionMode: AppState.ConnectionMode) -> WebSocketSessionBox? {
271-
guard url.scheme?.lowercased() == "wss" else { return nil }
309+
guard url.scheme?.lowercased() == "wss" else {
310+
self.tlsSessionCache.invalidate()
311+
return nil
312+
}
272313
let stableID = Self.tlsPinStoreKey(for: url)
273314
let stored = GatewayTLSStore.loadFingerprint(stableID: stableID)
274315
guard let params = Self.tlsParams(
275316
for: url,
276317
connectionMode: connectionMode,
277318
root: OpenClawConfigFile.loadDict(),
278319
storedFingerprint: stored)
279-
else { return nil }
280-
let session = GatewayTLSPinningSession(params: params)
281-
return WebSocketSessionBox(session: session)
320+
else {
321+
self.tlsSessionCache.invalidate()
322+
return nil
323+
}
324+
return self.tlsSessionCache.sessionBox(url: url, params: params)
282325
}
283326
}

apps/macos/Tests/OpenClawIPCTests/MacNodeModeCoordinatorTests.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,41 @@ struct MacNodeModeCoordinatorTests {
8989
#expect(params.allowTOFU == false)
9090
}
9191

92+
@Test func `tls session cache reuses session box for unchanged params`() throws {
93+
let url = try #require(URL(string: "wss://gateway.example.com"))
94+
var cache = MacNodeGatewayTLSSessionCache()
95+
let params = try #require(MacNodeModeCoordinator.tlsParams(
96+
for: url,
97+
connectionMode: .remote,
98+
root: ["gateway": ["remote": ["tlsFingerprint": "sha256:configured"]]],
99+
storedFingerprint: "stored"))
100+
101+
let first = cache.sessionBox(url: url, params: params)
102+
let second = cache.sessionBox(url: url, params: params)
103+
104+
#expect(ObjectIdentifier(first.session) == ObjectIdentifier(second.session))
105+
}
106+
107+
@Test func `tls session cache rebuilds session box when params change`() throws {
108+
let url = try #require(URL(string: "wss://gateway.example.com"))
109+
var cache = MacNodeGatewayTLSSessionCache()
110+
let firstParams = try #require(MacNodeModeCoordinator.tlsParams(
111+
for: url,
112+
connectionMode: .remote,
113+
root: ["gateway": ["remote": ["tlsFingerprint": "sha256:configured"]]],
114+
storedFingerprint: "stored"))
115+
let secondParams = try #require(MacNodeModeCoordinator.tlsParams(
116+
for: url,
117+
connectionMode: .remote,
118+
root: ["gateway": ["remote": ["tlsFingerprint": "sha256:rotated"]]],
119+
storedFingerprint: "stored"))
120+
121+
let first = cache.sessionBox(url: url, params: firstParams)
122+
let second = cache.sessionBox(url: url, params: secondParams)
123+
124+
#expect(ObjectIdentifier(first.session) != ObjectIdentifier(second.session))
125+
}
126+
92127
@Test func `auto repairs trusted tailscale serve pin mismatch`() throws {
93128
let url = try #require(URL(string: "wss://gateway.example.ts.net"))
94129
let failure = GatewayTLSValidationFailure(

0 commit comments

Comments
 (0)