Repository navigation
Add SSH remote Connections - #1408
Conversation
Verified web tokens are saved per host:port with Electron safeStorage in userData/remote-connections.json; the renderer only keeps a tokenSaved hint. A blank token reuses the saved one, a rejected saved token is deleted, Forget removes it, and Linux basic_text storage is refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An unexpected SSH exit no longer closes the remote window. The tunnel is respawned on the same loopback port with backoff (about a minute), so the window origin is unchanged and the page's wire client reconnects and resyncs by itself. The title shows Reconnecting… meanwhile. A rejected token or a non-Solenta port stops retrying; giving up closes the window with the last SSH error as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tunnel carries the web token, so force StrictHostKeyChecking=yes even when ~/.ssh/config relaxes it for the host: an unknown or changed host key now refuses to connect. Unknown host, changed key and password-only logins get an instruction instead of raw ssh stderr. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A blank token now reads userData/web-token on the host over SSH (macOS then Linux/XDG layout, under sh -c so the login shell does not matter), then falls back to the keychain copy. SSH refusals surface before a tunnel is spawned. When the forward is up but nothing listens on the remote port, Connect fails fast with "Start Solenta there with --serve-web" instead of timing out; it never launches the host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
End-to-end check against real OpenSSH (2026-10-03). No local sshd was running, so this used an unprivileged
CI is green on 844cda6. Marking ready for review. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 844cda63b8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| server.listen(0, "127.0.0.1", () => { | ||
| const port = server.address().port; | ||
| server.close((err) => err ? reject(err) : resolve(port)); | ||
| }); |
There was a problem hiding this comment.
Reserve the loopback listener until SSH owns it
When another process on the same machine claims this port after server.close() and before ssh -L binds it, waitForHost connects to that process and probeToken sends it the typed or keychain token. The SSH forward then fails later, but the token has already been disclosed, allowing a local untrusted process to access the remote Web host. Keep an exclusive reservation or obtain the forwarding port from SSH before authenticating.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 1bb7d77. The window now uses a front loopback port that Solenta holds for the window's whole life, piped to ssh's current forward. ssh gets a fresh port per spawn. Nothing probes or routes to it until ssh -v logs "Entering interactive session": with ExitOnForwardFailure, ssh only gets there after every bind succeeded. ("Local forwarding listening" is logged before bind(), as verified against OpenSSH 10.3 with a squatted port, so it is not used.) This also closes the same race in the reconnect gap, where the page's own WebSocket would have resent its token. New test: a squatter wins ssh's port and receives zero messages; with the gate removed, that test fails. Re-verified end to end against a real sshd.
Codex review on #1408: freeLoopbackPort released the port before ssh -L bound it, so a local process could take it and receive the probe's token; the reconnect gap had the same race for the page's own WebSocket. The window now talks to a front port this process holds for its whole life, piped to ssh's current forward. ssh gets a fresh port per spawn and is only probed or routed to after `ssh -v` reports "Entering interactive session", which with ExitOnForwardFailure means every bind succeeded. ("Local forwarding listening" is logged before bind(), so it is not used.) ssh's stderr is now read per line, debug lines are kept out of messages, and the failure is taken on 'close' so the last stderr line is not lost. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What changed
Settings → Connections opens a Solenta Web host (
--serve-web) on another machine through an SSH tunnel, in an isolated desktop window. Closes the remaining scope of #245.-Lforward to the host's web port, a WebSocket token check before any window opens, then a sandboxed window with its own partition.userData/web-tokenon the host over SSH (macOS, then Linux/XDG layout), then falls back to a copy encrypted withsafeStoragein the OS keychain. Linux plaintext (basic_text) storage is refused. A rejected keychain token is deleted. Forget removes it.StrictHostKeyChecking=yesoverrides a relaxed~/.ssh/config, so the token never goes to an unknown or changed host key. Unknown host, changed key and password-only logins get instructions instead of raw ssh stderr.Verification
tsc --noEmitclean. Connections and Settings renderer tests 64/64. Full renderer suite 2641/2641 (on32014f94).electron/test/remoteConnections.test.js: wrong-token rejection, keychain-only storage, token read over SSH, SSH refusal before any tunnel, fail-fast when nothing listens, reconnect on the same port, give-up after 6 retries. A mutation check (retries disabled) fails the test.sh/zsh/bashwith fake macOS, Linux and empty homes. The ssh error strings were captured from OpenSSH.waitFor/pid timeouts in untouched provider/MCP tests, which pass in isolation.🤖 Generated with Claude Code