Repository navigation
Fix Cloud discovery stalls and private address fallback #12266
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
02ed751
refactor: isolate surface catalog query ownership
austinywang 39920fb
test: reproduce new Cloud machine catalog discovery race
austinywang 3636250
fix: discover new Cloud machines before reading their catalog
austinywang b4da292
fix: race cloud private addresses before choosing a connection path
austinywang 6d11b79
Merge remote-tracking branch 'origin/fix-cloud-snapshot-connect-readi…
austinywang 4efb148
fix: serve the cloud desktop over both private address families
austinywang 3560644
fix: reuse the working family for successive cloud port connections
austinywang cb672b4
fix: record verified dual-stack desktop snapshot ladder
austinywang 3f43aff
refactor: inject Cloud discovery and provider refresh operations
austinywang c4f3152
test: cover Cloud discovery stalls and abandoned hub dials
austinywang 1446992
test: encode the hub fixture IPv6 address explicitly
austinywang a977941
fix: bound Cloud discovery and cancel abandoned tunnel dials
austinywang 028c4be
test: reproduce stale primary route with a sole IPv6 candidate
austinywang 5a3b354
Merge remote-tracking branch 'origin/fix-cloud-snapshot-connect-readi…
austinywang 550d841
test: cover discovery retirement and browser address reuse
austinywang d6a4127
test: split browser address reuse coverage into its own suite
austinywang 406748a
fix: retire Cloud discovery waiters and preserve sibling refreshes
austinywang c9461e9
Merge remote-tracking branch 'origin/fix-cloud-snapshot-connect-readi…
austinywang 39b6c04
fix: use the sole current private address instead of a stale route
austinywang 2c558f0
Merge origin/main and reconcile Cloud test project entries
austinywang b93855d
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang d3e0d7a
test: cover partial Cloud routes and post-sign-out discovery
austinywang 6c9200d
fix: preserve Cloud fallback routes and serialize account discovery
austinywang bb8a40a
test: isolate Cloud registry notifications across parallel suites
austinywang 68d7c50
test: reproduce publishing a Cloud VM before network announcement
austinywang File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| import Foundation | ||
| import Network | ||
|
|
||
| extension CloudMachineLinkManager { | ||
| /// Selects a working family using the same SOCKS connector as port forwards. | ||
| /// The probe sends no daemon request and closes its stream before the real | ||
| /// encrypted link starts. Failed probes propagate to the caller's retry policy, | ||
| /// so the next attempt considers every current family again. | ||
| func resolvedPrivateRoute( | ||
| machineID: String, | ||
| through hub: CloudWireGuardHub.Ready, | ||
| fallbackRoute: String? = nil, | ||
| addresses freshAddresses: [String] = [] | ||
| ) async throws -> String { | ||
| try Task.checkCancellation() | ||
| let freshAddresses = freshAddresses.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } | ||
| .filter { !$0.isEmpty } | ||
| let freshFamilies = Set(freshAddresses.map { $0.contains(":") }) | ||
| // Attach can omit one family. Preserve that family from discovery, but | ||
| // never retain an old address in a family that attach has replaced. | ||
| let storedAddresses = privateAddresses(for: machineID).filter { | ||
| !freshFamilies.contains($0.contains(":")) | ||
| } | ||
| var seen = Set<String>() | ||
| let candidates = (freshAddresses + storedAddresses).filter { seen.insert($0).inserted } | ||
| let addresses = candidates.filter { | ||
| CloudWireGuardHub.routesHost($0, enrolledRoutes: hub.routes) | ||
| } | ||
| guard let primary = addresses.first else { | ||
| guard candidates.isEmpty, | ||
| let route = fallbackRoute ?? privateRoute(for: machineID), | ||
| let host = IPNetworkPrefix.routeHost(route), | ||
| CloudWireGuardHub.routesHost(host, enrolledRoutes: hub.routes) else { | ||
| throw ManagerError.privateRouteRequired(machineID) | ||
| } | ||
| return route | ||
| } | ||
| // Fresh discovery or the enrolled routes can leave a single candidate. | ||
| // Use it directly rather than returning an older address-family route. | ||
| guard addresses.count > 1 else { | ||
| let host = primary.contains(":") ? "[\(primary)]" : primary | ||
| return "ws://\(host):1337/v1/link" | ||
| } | ||
| let connected = try await CloudHubConnector().connect( | ||
| endpoint: .unix(path: hub.socketPath), | ||
| target: CloudPortForwardTarget(host: primary, port: 1337, fallbackHosts: Array(addresses.dropFirst())), | ||
| queue: DispatchQueue.global(qos: .userInitiated) | ||
| ) | ||
| connected.connection.cancel() | ||
| let host = connected.host.contains(":") ? "[\(connected.host)]" : connected.host | ||
| return "ws://\(host):1337/v1/link" | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import Network | ||
|
|
||
| /// The successful private-address dial and the hub stream carrying its bytes. | ||
| struct CloudHubConnection: Sendable { | ||
| let connection: NWConnection | ||
| let host: String | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| import Foundation | ||
| import Network | ||
|
|
||
| /// Connects to the first working private address through one claimed hub. | ||
| /// A family can blackhole independently of the other after a VM joins its VPC. | ||
| /// Race actual SOCKS CONNECT handshakes, retaining the winning stream and closing | ||
| /// every loser before returning, so terminal and browser callers share the policy. | ||
| struct CloudHubConnector: Sendable { | ||
| var timeout: Duration = .seconds(15) | ||
| /// A cancellable head start for the preferred family, driven by the injected clock. | ||
| var fallbackDelay: Duration = .milliseconds(250) | ||
| var clock: any Clock<Duration> = ContinuousClock() | ||
|
|
||
| #if compiler(>=6.2) | ||
| @concurrent | ||
| #else | ||
| @Sendable | ||
| #endif | ||
| func connect( | ||
| endpoint: NWEndpoint, | ||
| target: CloudPortForwardTarget, | ||
| queue: DispatchQueue | ||
| ) async throws -> CloudHubConnection { | ||
| let candidates = target.hosts.map { host in | ||
| CloudHubConnection(connection: NWConnection(to: endpoint, using: .tcp), host: host) | ||
| } | ||
| return try await withTaskCancellationHandler { | ||
| try await withThrowingTaskGroup(of: Result<CloudHubConnection, any Error>.self) { group in | ||
| for (index, candidate) in candidates.enumerated() { | ||
| group.addTask { | ||
| do { | ||
| if index > 0 { try await clock.sleep(for: fallbackDelay) } | ||
| try Task.checkCancellation() | ||
| try await handshake(candidate.connection, host: candidate.host, port: target.port, queue: queue) | ||
| try Task.checkCancellation() | ||
| return .success(candidate) | ||
| } catch { | ||
| candidate.connection.cancel() | ||
| return .failure(error) | ||
| } | ||
| } | ||
| } | ||
| defer { group.cancelAll() } | ||
| var lastError: any Error = CancellationError() | ||
| while let result = try await group.next() { | ||
| switch result { | ||
| case .success(let connected): | ||
| for other in candidates where other.connection !== connected.connection { | ||
| other.connection.cancel() | ||
| } | ||
| if Task.isCancelled { | ||
| connected.connection.cancel() | ||
| throw CancellationError() | ||
| } | ||
| return connected | ||
| case .failure(let error): | ||
| lastError = error | ||
| } | ||
| } | ||
| throw lastError | ||
| } | ||
| } onCancel: { | ||
| for candidate in candidates { candidate.connection.cancel() } | ||
| } | ||
| } | ||
|
|
||
| #if compiler(>=6.2) | ||
| @concurrent | ||
| #else | ||
| @Sendable | ||
| #endif | ||
| private func handshake(_ connection: NWConnection, host: String, port: Int, queue: DispatchQueue) async throws { | ||
| try await withTaskCancellationHandler { | ||
| try await withThrowingTaskGroup(of: Void.self) { group in | ||
| group.addTask { | ||
| try await connection.startAndWaitUntilReady(queue: queue) | ||
| try await CloudPortForwardRelay.connect(connection, to: CloudPortForwardTarget(host: host, port: port)) | ||
| } | ||
| group.addTask { | ||
| // A real handshake deadline; completion cancels this child | ||
| // and expiry cancels the socket to unblock Network callbacks. | ||
| try await clock.sleep(for: timeout) | ||
| connection.cancel() | ||
| throw CloudPortForwardRelay.RelayError.handshakeTimedOut(timeout) | ||
| } | ||
| defer { group.cancelAll() } | ||
| try await group.next() | ||
| } | ||
| } onCancel: { | ||
| connection.cancel() | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.