Repository navigation
Mobile relay v2: Durable Object transport with direct Stack auth and end-to-end session admission #10963
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
Open
lawrencecchen
wants to merge
9
commits into
main
Choose a base branch
from
feat-hostrelay-transport
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Mobile relay v2: Durable Object transport with direct Stack auth and end-to-end session admission #10963
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
0c6204d
feat(mobile): HostRelay Durable Object transport (relay worker, ticke…
lawrencecchen 5bd1731
fix(mobile-relay): server-controlled relay URL on the client dial; ig…
lawrencecchen 1193655
docs: mobile relay transport overview
lawrencecchen 2fc4767
fix(ios): resolve relay API base URL on the main actor at composition…
lawrencecchen 5830bca
tools: add one-command mobile latency measurement
lawrencecchen 064466f
ios: pipeline ordered terminal input over the relay route
lawrencecchen d36d53c
relay v2: direct-to-DO Stack auth, end-to-end session admission, no t…
lawrencecchen 2103d45
tools: measure local durable object relay latency
lawrencecchen 25e10ae
fix(mobile-relay): fail closed and replace client connections
lawrencecchen 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| # CI/CD for the mobile relay service (workers/mobile-relay). | ||
| # | ||
| # Manual runs test and deploy the service to Cloudflare. `wrangler deploy` | ||
| # applies the Durable Object migrations declared in wrangler.toml atomically | ||
| # with the code upload. | ||
| # | ||
| # The `target` input picks the worker: `prod` (default) deploys | ||
| # `cmux-mobile-relay` on mr.cmux.dev; `dev` deploys `cmux-mobile-relay-dev` | ||
| # from wrangler.dev.toml on workers.dev. | ||
| # | ||
| # Required repository secrets (deploy job): | ||
| # CLOUDFLARE_API_TOKEN API token with Workers Scripts:Edit on the account | ||
| # CLOUDFLARE_ACCOUNT_ID the Cloudflare account id | ||
| # | ||
| # The worker holds no secrets: connect auth verifies Stack access tokens | ||
| # with the public project configuration in wrangler[.dev].toml [vars]. | ||
|
|
||
| name: mobile-relay | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| target: | ||
| description: "Worker to deploy" | ||
| type: choice | ||
| options: | ||
| - prod | ||
| - dev | ||
| default: prod | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| test: | ||
| runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} | ||
| defaults: | ||
| run: | ||
| working-directory: workers/mobile-relay | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
|
|
||
| - name: Setup Bun | ||
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 | ||
|
|
||
| # Wrangler requires Node >= 22; the Blacksmith ubuntu-2404 image ships | ||
| # Node 20, so pin it explicitly. | ||
| - name: Setup Node | ||
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | ||
| with: | ||
| node-version: "22" | ||
|
|
||
| - name: Install dependencies | ||
| run: bun install --frozen-lockfile | ||
|
|
||
| - name: Generated protocol drift check | ||
| run: bun run generate:check | ||
|
|
||
| - name: Typecheck | ||
| run: bun run typecheck | ||
|
|
||
| - name: Unit tests | ||
| run: bun test | ||
|
|
||
| - name: Wrangler dry-run build | ||
| run: bunx wrangler deploy --dry-run --outdir dist | ||
|
|
||
| deploy: | ||
| if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' | ||
| needs: test | ||
| runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} | ||
| concurrency: | ||
| group: mobile-relay-deploy | ||
| cancel-in-progress: false | ||
| defaults: | ||
| run: | ||
| working-directory: workers/mobile-relay | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
|
|
||
| - name: Setup Bun | ||
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 | ||
|
|
||
| # Wrangler requires Node >= 22; Blacksmith ubuntu-2404 ships Node 20. | ||
| - name: Setup Node | ||
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | ||
| with: | ||
| node-version: "22" | ||
|
|
||
| - name: Install dependencies | ||
| run: bun install --frozen-lockfile | ||
|
|
||
| - name: Check Cloudflare secrets | ||
| env: | ||
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | ||
| run: | | ||
| missing=() | ||
| [ -n "$CLOUDFLARE_API_TOKEN" ] || missing+=(CLOUDFLARE_API_TOKEN) | ||
| [ -n "$CLOUDFLARE_ACCOUNT_ID" ] || missing+=(CLOUDFLARE_ACCOUNT_ID) | ||
| if [ "${#missing[@]}" -gt 0 ]; then | ||
| echo "::error::Mobile relay deploy needs repository secrets ${missing[*]}." \ | ||
| "Create an API token with Workers Scripts:Edit on the Cloudflare" \ | ||
| "account and add both secrets in repo Settings -> Secrets and" \ | ||
| "variables -> Actions (see workers/mobile-relay/README.md)." | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| exit 1 | ||
| fi | ||
|
|
||
| - name: Deploy (applies DO migrations atomically) | ||
| # The target reaches the shell via env, never template interpolation, | ||
| # and any value other than the two known targets fails closed. | ||
| run: | | ||
| case "$DEPLOY_TARGET" in | ||
| dev) bunx wrangler deploy --config wrangler.dev.toml ;; | ||
| prod) bunx wrangler deploy ;; | ||
| *) echo "::error::Unsupported deployment target '$DEPLOY_TARGET'"; exit 1 ;; | ||
| esac | ||
| env: | ||
| DEPLOY_TARGET: ${{ inputs.target }} | ||
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | ||
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
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,34 @@ | ||
| // swift-tools-version: 6.0 | ||
|
|
||
| import PackageDescription | ||
|
|
||
| let package = Package( | ||
| name: "CmuxRelayTransport", | ||
| platforms: [ | ||
| .iOS(.v18), | ||
| .macOS(.v14), | ||
| ], | ||
| products: [ | ||
| .library( | ||
| name: "CmuxRelayTransport", | ||
| targets: ["CmuxRelayTransport"] | ||
| ), | ||
| ], | ||
| dependencies: [ | ||
| .package(path: "../CMUXMobileCore"), | ||
| ], | ||
| targets: [ | ||
| .target( | ||
| name: "CmuxRelayTransport", | ||
| dependencies: [ | ||
| .product(name: "CMUXMobileCore", package: "CMUXMobileCore"), | ||
| ], | ||
| swiftSettings: [.swiftLanguageMode(.v6)] | ||
| ), | ||
| .testTarget( | ||
| name: "CmuxRelayTransportTests", | ||
| dependencies: ["CmuxRelayTransport"], | ||
| swiftSettings: [.swiftLanguageMode(.v6)] | ||
| ), | ||
| ] | ||
| ) |
147 changes: 147 additions & 0 deletions
147
...ared/CmuxRelayTransport/Sources/CmuxRelayTransport/Generated/RelayProtocolGenerated.swift
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,147 @@ | ||
| // GENERATED by workers/mobile-relay/tools/generate.ts — DO NOT EDIT. | ||
| // Source of truth: workers/mobile-relay/src/protocol.ts (Effect Schema). | ||
| // Regenerate with `bun run generate` in workers/mobile-relay. | ||
|
|
||
| import Foundation | ||
|
|
||
| /// Wire constants shared with the relay worker. See src/protocol.ts for the | ||
| /// full contract (framing, session policy, close codes). | ||
| public enum RelayProtocol { | ||
| public static let version = 2 | ||
| public static let dataFrameType: UInt8 = 1 | ||
| public static let dataHeaderBytes = 5 | ||
| public static let hostSessionID: UInt32 = 0 | ||
| public static let channelRPC: UInt8 = 0 | ||
| public static let channelTerminal: UInt8 = 1 | ||
| public static let channelSimulator: UInt8 = 2 | ||
| public static let channelCredit: UInt8 = 3 | ||
| public static let maxDataPayloadBytes = 262144 | ||
| public static let maxControlBytes = 4096 | ||
| public static let sessionMaxAgeMilliseconds = 3600000 | ||
| public static let pingText = "ping" | ||
| public static let pongText = "pong" | ||
| public static let stackAccessHeaderName = "x-cmux-stack-access" | ||
| public static let roleHeaderName = "x-cmux-role" | ||
| public static let hostDeviceHeaderName = "x-cmux-host-device" | ||
| public static let deviceHeaderName = "x-cmux-device" | ||
| public static let connectPath = "/v1/connect" | ||
| public static let defaultRelayURL = "wss://mr.cmux.dev/v1/connect" | ||
| public static let byeSuperseded = "superseded" | ||
| public static let byeExpired = "expired" | ||
| public static let byeProtocolError = "protocol_error" | ||
| public static let byeAtCapacity = "at_capacity" | ||
| public static let byeHostClosed = "host_closed" | ||
| } | ||
|
|
||
| public enum RelayRole: String, Codable, Sendable { | ||
| case host | ||
| case client | ||
| } | ||
|
|
||
| public struct RelayWelcome: Codable, Sendable, Equatable { | ||
| public static let type = "welcome" | ||
| public var t: String = Self.type | ||
| public var v: Int | ||
| public var role: RelayRole | ||
| public var sessionId: Int | ||
| public var deadline: Double | ||
| public var hostPresent: Bool | ||
| public init(v: Int, role: RelayRole, sessionId: Int, deadline: Double, hostPresent: Bool) { | ||
| self.v = v | ||
| self.role = role | ||
| self.sessionId = sessionId | ||
| self.deadline = deadline | ||
| self.hostPresent = hostPresent | ||
| } | ||
| } | ||
|
|
||
| public struct RelayPeerJoined: Codable, Sendable, Equatable { | ||
| public static let type = "peer_joined" | ||
| public var t: String = Self.type | ||
| public var sessionId: Int | ||
| public var deviceId: String | ||
| public init(sessionId: Int, deviceId: String) { | ||
| self.sessionId = sessionId | ||
| self.deviceId = deviceId | ||
| } | ||
| } | ||
|
|
||
| public struct RelayPeerLeft: Codable, Sendable, Equatable { | ||
| public static let type = "peer_left" | ||
| public var t: String = Self.type | ||
| public var sessionId: Int | ||
| public var reason: String | ||
| public init(sessionId: Int, reason: String) { | ||
| self.sessionId = sessionId | ||
| self.reason = reason | ||
| } | ||
| } | ||
|
|
||
| public struct RelayRefreshAck: Codable, Sendable, Equatable { | ||
| public static let type = "refresh_ack" | ||
| public var t: String = Self.type | ||
| public var deadline: Double | ||
| public init(deadline: Double) { | ||
| self.deadline = deadline | ||
| } | ||
| } | ||
|
|
||
| public struct RelayBye: Codable, Sendable, Equatable { | ||
| public static let type = "bye" | ||
| public var t: String = Self.type | ||
| public var code: String | ||
| public var reason: String | ||
| public init(code: String, reason: String) { | ||
| self.code = code | ||
| self.reason = reason | ||
| } | ||
| } | ||
|
|
||
| public struct RelayRefresh: Codable, Sendable, Equatable { | ||
| public static let type = "refresh" | ||
| public var t: String = Self.type | ||
| public var accessToken: String | ||
| public init(accessToken: String) { | ||
| self.accessToken = accessToken | ||
| } | ||
| } | ||
|
|
||
| public struct RelayCloseSession: Codable, Sendable, Equatable { | ||
| public static let type = "close_session" | ||
| public var t: String = Self.type | ||
| public var sessionId: Int | ||
| public init(sessionId: Int) { | ||
| self.sessionId = sessionId | ||
| } | ||
| } | ||
|
|
||
| /// Every message the relay can send. Decode with `RelayServerMessage.decode(_:)`; | ||
| /// unknown or malformed input returns nil (a same-version relay never sends it). | ||
| public enum RelayServerMessage: Sendable, Equatable { | ||
| case welcome(RelayWelcome) | ||
| case peerJoined(RelayPeerJoined) | ||
| case peerLeft(RelayPeerLeft) | ||
| case refreshAck(RelayRefreshAck) | ||
| case bye(RelayBye) | ||
|
|
||
| private struct Probe: Decodable { let t: String } | ||
|
|
||
| public static func decode(_ data: Data) -> RelayServerMessage? { | ||
| let decoder = JSONDecoder() | ||
| guard let probe = try? decoder.decode(Probe.self, from: data) else { return nil } | ||
| switch probe.t { | ||
| case RelayWelcome.type: | ||
| return (try? decoder.decode(RelayWelcome.self, from: data)).map(RelayServerMessage.welcome) | ||
| case RelayPeerJoined.type: | ||
| return (try? decoder.decode(RelayPeerJoined.self, from: data)).map(RelayServerMessage.peerJoined) | ||
| case RelayPeerLeft.type: | ||
| return (try? decoder.decode(RelayPeerLeft.self, from: data)).map(RelayServerMessage.peerLeft) | ||
| case RelayRefreshAck.type: | ||
| return (try? decoder.decode(RelayRefreshAck.self, from: data)).map(RelayServerMessage.refreshAck) | ||
| case RelayBye.type: | ||
| return (try? decoder.decode(RelayBye.self, from: data)).map(RelayServerMessage.bye) | ||
| default: | ||
| return nil | ||
| } | ||
| } | ||
| } |
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Validate the selected worker configuration during the dry run.
When
targetisdev, Line 68 still validateswrangler.toml. A brokenwrangler.dev.tomlcan passtestand fail only after the deploy job starts. Select the dry-run configuration with the same target branch used by Lines 116-120.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents