Repository navigation
Iroh: full-stack coverage, a client that builds without Zig, and an iOS app #9237
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
Closed
Closed
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
de87ab2
Add full-stack Iroh end-to-end coverage
lawrencecchen 036c710
Let a client build without the daemon's Zig dependency
lawrencecchen 0ba72d3
Add cmux-remote-mobile, a C ABI over the client for iOS
lawrencecchen 34c00a6
Add a brand-new iOS client for the remote daemon over Iroh
lawrencecchen 41058ee
fix(tui): accept borrowed paths in Ghostty build (#9986)
lawrencecchen a7e63ee
Fix cross-platform workspace mode Clippy lint (#9988)
lawrencecchen 6b4156a
Make initial route timeout tests owner-driven (#9993)
lawrencecchen dcf8132
Test SSH bootstrap shutdown at its child owner (#9996)
lawrencecchen 676b0b1
Update Cloudflare relay dependencies (#10000)
lawrencecchen 5d99c7e
Keep provider selection pending until connectable (#10012)
lawrencecchen cf6619b
test: inject memory-instrumented transport workload (#10025)
lawrencecchen 4f327a1
Await published browser tab state in TUI smoke test (#10026)
lawrencecchen d3b8c2c
Merge current remote daemon base into Iroh iOS PR
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
Large diffs are not rendered by default.
Oops, something went wrong.
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,2 @@ | ||
| # Generated by `xcodegen generate` from project.yml. | ||
| CmuxRemote.xcodeproj/ |
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,38 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
| <plist version="1.0"> | ||
| <dict> | ||
| <key>CFBundleDevelopmentRegion</key> | ||
| <string>$(DEVELOPMENT_LANGUAGE)</string> | ||
| <key>CFBundleDisplayName</key> | ||
| <string>cmux remote</string> | ||
| <key>CFBundleExecutable</key> | ||
| <string>$(EXECUTABLE_NAME)</string> | ||
| <key>CFBundleIdentifier</key> | ||
| <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> | ||
| <key>CFBundleInfoDictionaryVersion</key> | ||
| <string>6.0</string> | ||
| <key>CFBundleName</key> | ||
| <string>$(PRODUCT_NAME)</string> | ||
| <key>CFBundlePackageType</key> | ||
| <string>APPL</string> | ||
| <key>CFBundleShortVersionString</key> | ||
| <string>1.0</string> | ||
| <key>CFBundleVersion</key> | ||
| <string>1</string> | ||
| <key>NSBonjourServices</key> | ||
| <array> | ||
| <string>_cmux._udp</string> | ||
| </array> | ||
| <key>NSLocalNetworkUsageDescription</key> | ||
| <string>cmux connects directly to your computer on this network instead of relaying through the internet.</string> | ||
| <key>UILaunchScreen</key> | ||
| <dict/> | ||
| <key>UISupportedInterfaceOrientations</key> | ||
| <array> | ||
| <string>UIInterfaceOrientationPortrait</string> | ||
| <string>UIInterfaceOrientationLandscapeLeft</string> | ||
| <string>UIInterfaceOrientationLandscapeRight</string> | ||
| </array> | ||
| </dict> | ||
| </plist> |
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,64 @@ | ||
| # cmux remote for iOS | ||
|
|
||
| A phone client for the cmux remote daemon, over Iroh. | ||
|
|
||
| It exists to exercise the transport on the network a phone actually has: NAT'd, | ||
| changing between cellular and Wi-Fi, and often unable to reach the daemon | ||
| directly at all. Those are the conditions the relay path and session resume are | ||
| for, and they do not occur on a loopback test. | ||
|
|
||
| ## What it does | ||
|
|
||
| Paste a `cmux://enroll/...` invitation, connect, and get a shell on the machine | ||
| that issued it. The status bar shows whether Iroh settled on a direct or relayed | ||
| path, and counts session resumes, so a walk out of Wi-Fi range is visible rather | ||
| than inferred. | ||
|
|
||
| The path picker forces direct-only or relay-only. Automatic is what a user would | ||
| run; the constrained modes are there so a failure can be attributed to one path | ||
| instead of "the network". | ||
|
|
||
| ## Architecture | ||
|
|
||
| The app implements none of the daemon protocol. Enrollment is a | ||
| PSK-authenticated Noise handshake, sessions are mutually authenticated and | ||
| resumable, frames carry per-lane sequence numbers with bounded replay, and Iroh | ||
| adds path selection and relay fallback underneath. A Swift reimplementation | ||
| would be a second set of bugs in exactly the parts that are hardest to test, so | ||
| the app links `cmux-remote-mobile`, a C ABI over the same Rust client the TUI | ||
| uses. | ||
|
|
||
| It also carries no VT parser. The daemon keeps the terminal model and answers | ||
| `SnapshotProcessTerminal` with styled runs, so `TerminalScreen` only lays out a | ||
| monospaced grid. Wrapping, scroll regions, and character sets stay on the side | ||
| that already got them right. | ||
|
|
||
| ## Build | ||
|
|
||
| ```bash | ||
| brew install xcodegen | ||
| cd cmux-tui/apps/ios | ||
| xcodegen generate | ||
| open CmuxRemote.xcodeproj | ||
| ``` | ||
|
|
||
| The Rust archive builds from a pre-build script, so a plain Xcode build is | ||
| enough. No Zig toolchain is involved: `cmux-remote-mobile` depends on | ||
| `cmux-remote` with `default-features = false`, which leaves out the daemon's | ||
| workspace service and with it libghostty-vt. | ||
|
|
||
| `CmuxRemote.xcodeproj` is generated and not committed. | ||
|
|
||
| ## Getting an invitation | ||
|
|
||
| On the machine you want to reach: | ||
|
|
||
| ```bash | ||
| cmux-tui daemon invite | ||
| ``` | ||
|
|
||
| It prints a `cmux://enroll/...` URI that expires in five minutes and carries the | ||
| daemon's public key plus route hints. The key is what the phone pins during the | ||
| handshake; the hints only say where to try, so a wrong or stale hint cannot | ||
| redirect the session to another host. The daemon asks the machine's owner to | ||
| approve the device before the first session completes. |
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 @@ | ||
| #import "cmux_remote_mobile.h" |
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,98 @@ | ||
| import SwiftUI | ||
|
|
||
| @main | ||
| struct CmuxRemoteApp: App { | ||
| var body: some Scene { | ||
| WindowGroup { | ||
| SessionView() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @MainActor | ||
| @Observable | ||
| final class SessionModel { | ||
| enum Phase: Equatable { | ||
| case disconnected | ||
| case connecting | ||
| case connected | ||
| case failed(String) | ||
| } | ||
|
|
||
| var invitation = "" | ||
| var pathMode = RemoteClient.PathMode.auto | ||
| var root = "~" | ||
| var phase = Phase.disconnected | ||
| var terminal: TerminalSnapshot? | ||
| var connection: ConnectionSnapshot? | ||
|
|
||
| private let client = RemoteClient() | ||
| private var pump: Task<Void, Never>? | ||
| private var grid: (cols: UInt16, rows: UInt16) = (80, 24) | ||
|
|
||
| func connect() { | ||
| guard phase != .connecting else { return } | ||
| phase = .connecting | ||
| let invitation = invitation.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| let pathMode = pathMode | ||
| let root = root | ||
| let grid = grid | ||
| let device = UIDevice.current.name | ||
|
|
||
| Task { | ||
| do { | ||
| try await client.connect( | ||
| invitation: invitation, deviceName: device, pathMode: pathMode) | ||
| try await client.openTerminal(root: root, cols: grid.cols, rows: grid.rows) | ||
| phase = .connected | ||
| startPump() | ||
| } catch { | ||
| phase = .failed(error.localizedDescription) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func disconnect() { | ||
| pump?.cancel() | ||
| pump = nil | ||
| terminal = nil | ||
| connection = nil | ||
| phase = .disconnected | ||
| Task { await client.disconnect() } | ||
| } | ||
|
|
||
| func send(_ text: String) { | ||
| Task { try? await client.write(text) } | ||
| } | ||
|
|
||
| /// Match the remote PTY to what the phone can actually show. Called on | ||
| /// rotation and whenever the keyboard changes the visible area. | ||
| func resize(to grid: (cols: UInt16, rows: UInt16)) { | ||
| guard grid != self.grid else { return } | ||
| self.grid = grid | ||
| guard phase == .connected else { return } | ||
| Task { | ||
| try? await client.resize(cols: grid.cols, rows: grid.rows) | ||
| terminal = await client.terminal() | ||
| } | ||
| } | ||
|
|
||
| /// Wait on output, then re-read the model. Blocking on the read rather than | ||
| /// polling on a timer means an idle shell costs nothing and a busy one | ||
| /// refreshes as fast as bytes arrive. | ||
| private func startPump() { | ||
| pump?.cancel() | ||
| pump = Task { [client] in | ||
| var lastSequence: UInt64 = .max | ||
| while !Task.isCancelled { | ||
| let produced = await client.readOutput() != nil | ||
| if let snapshot = await client.terminal(), | ||
| produced || snapshot.throughSequence != lastSequence { | ||
| lastSequence = snapshot.throughSequence | ||
| terminal = snapshot | ||
| } | ||
| connection = await client.connection() | ||
| } | ||
| } | ||
| } | ||
| } | ||
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.
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.
Idle pump still polls RPCs
Medium Severity
The pump comment claims an idle shell costs nothing, but after every
readOutputtimeout it still awaitsterminal()andconnection(), each of which crosses into Rust and hits the daemon. On a phone that becomes a SnapshotProcessTerminal plus diagnostics RPC about every 250ms while the shell is idle.Reviewed by Cursor Bugbot for commit 41058ee. Configure here.